diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md
index 0d1be80..721c951 100644
--- a/.agents/AGENTS.md
+++ b/.agents/AGENTS.md
@@ -9,7 +9,7 @@
2. **Reconciler Pattern**:
- Fetch the object first; if not found (`apierrors.IsNotFound`), return `ctrl.Result{}` immediately — it was deleted.
- Always update `status` last, after all child resources are reconciled. Never update status mid-reconcile.
- - Use `controllerutil.CreateOrUpdate` for all owned child resources (Deployment, Service, ServiceMonitor, HPA).
+ - Use `controllerutil.CreateOrUpdate` for all owned child resources (Deployment, Service, ServiceMonitor, HPA, HTTPRoute).
- Requeue transient errors with `ctrl.Result{RequeueAfter: ...}`, not `ctrl.Result{Requeue: true}`.
3. **Owner References & Finalizers**:
diff --git a/.agents/skills/agentrax-context/SKILL.md b/.agents/skills/agentrax-context/SKILL.md
index 4105f90..8c4cedd 100644
--- a/.agents/skills/agentrax-context/SKILL.md
+++ b/.agents/skills/agentrax-context/SKILL.md
@@ -56,8 +56,11 @@ description: Project context and settled architecture decisions for the Agentrax
| Canary | Prometheus unreachable during pause | Fail-safe rollback after 60 s; never hang |
| Canary | Second rollout triggered mid-rollout | Webhook rejects; never run two concurrent canaries on the same `AgentDeployment` |
| Canary | HPA during active canary | Pause stable HPA; no canary HPA; resume only after promote/rollback |
+| Canary | Out-of-band deletion of HTTPRoute/Deployment | Self-healed on next reconcile cycle in `Step()` preserving active traffic split |
+| Canary | Rollout failed or aborted | Operator sets `RolloutFailed`, preserves `status.canaryVersion`, no retry loop |
| Quota | Two concurrent near-limit creates | In-flight reservation; one wins, one is rejected |
| Quota | Quota lowered below current usage | Set `OverQuota` condition; never forcibly delete existing resources |
+| Quota | `TenantQuota` deleted while agents exist | Surface `TenantQuotaNotFound` on `QuotaLimited` condition; never crash |
| MCP | Ungraceful pod termination | TTL/heartbeat expires the entry within one TTL window (default 90 s) |
| MCP | Pod `Ready` but MCP handshake fails | Do not register; surface `MCPHandshakeFailed` condition |
| Deletion | `AgentDeployment` deleted | Finalizer ensures MCP deregistration before `Service` is GC'd |
diff --git a/api/v1alpha1/agentdeployment_types.go b/api/v1alpha1/agentdeployment_types.go
index f93a2c9..bdf01ca 100644
--- a/api/v1alpha1/agentdeployment_types.go
+++ b/api/v1alpha1/agentdeployment_types.go
@@ -162,6 +162,21 @@ type AgentDeploymentStatus struct {
// CanaryWeight is the current percentage of traffic routed to the canary (0–100).
CanaryWeight int32 `json:"canaryWeight,omitempty"`
+ // CanaryStepIndex is the index of the currently executing rollout step.
+ // Persisted so the state machine survives operator restarts.
+ // +optional
+ CanaryStepIndex int `json:"canaryStepIndex,omitempty"`
+
+ // PauseStartedAt records when the current pause step began.
+ // Used to enforce maximum pause extensions and the fail-safe rollback timeout.
+ // +optional
+ PauseStartedAt *metav1.Time `json:"pauseStartedAt,omitempty"`
+
+ // PromUnreachableSince records when Prometheus last became unreachable.
+ // When non-nil and age exceeds FailSafeTimeout, a fail-safe rollback fires.
+ // +optional
+ PromUnreachableSince *metav1.Time `json:"promUnreachableSince,omitempty"`
+
// Registered is true when this agent is currently registered in the MCP registry.
Registered bool `json:"registered,omitempty"`
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 4943ea5..7177715 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -119,6 +119,14 @@ func (in *AgentDeploymentSpec) DeepCopy() *AgentDeploymentSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentDeploymentStatus) DeepCopyInto(out *AgentDeploymentStatus) {
*out = *in
+ if in.PauseStartedAt != nil {
+ in, out := &in.PauseStartedAt, &out.PauseStartedAt
+ *out = (*in).DeepCopy()
+ }
+ if in.PromUnreachableSince != nil {
+ in, out := &in.PromUnreachableSince, &out.PromUnreachableSince
+ *out = (*in).DeepCopy()
+ }
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
diff --git a/cmd/main.go b/cmd/main.go
index f8afa3b..7a6957a 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -21,6 +21,7 @@ import (
"crypto/tls"
"flag"
"os"
+ "time"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
@@ -39,10 +40,13 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
agentraxv1alpha1 "github.com/gitcommitankit/agentrax/api/v1alpha1"
"github.com/gitcommitankit/agentrax/internal/controller"
+ "github.com/gitcommitankit/agentrax/internal/metrics"
"github.com/gitcommitankit/agentrax/internal/quota"
+ "github.com/gitcommitankit/agentrax/internal/rollout"
agentraxwebhook "github.com/gitcommitankit/agentrax/internal/webhook"
// +kubebuilder:scaffold:imports
)
@@ -58,6 +62,7 @@ func init() {
utilruntime.Must(autoscalingv2.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
utilruntime.Must(monitoringv1.AddToScheme(scheme))
+ utilruntime.Must(gatewayv1.Install(scheme))
utilruntime.Must(agentraxv1alpha1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
@@ -71,6 +76,9 @@ func main() {
var secureMetrics bool
var enableHTTP2 bool
var gpuResourceName string
+ var prometheusURL string
+ var gatewayName string
+ var gatewayNamespace string
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -84,6 +92,13 @@ func main() {
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.StringVar(&gpuResourceName, "gpu-resource-name", quota.DefaultGPUResourceName,
"Kubernetes resource name used to count GPU units in AgentDeployment resource limits.")
+ flag.StringVar(&prometheusURL, "prometheus-url", "",
+ "URL of the Prometheus HTTP API (e.g. http://prometheus-operated.monitoring.svc:9090). "+
+ "Required for Canary rollout strategy; if empty, canary is unavailable.")
+ flag.StringVar(&gatewayName, "gateway-name", "agentrax-gateway",
+ "Name of the Gateway API Gateway object used for canary traffic splitting.")
+ flag.StringVar(&gatewayNamespace, "gateway-namespace", "agentrax-system",
+ "Namespace of the Gateway API Gateway object used for canary traffic splitting.")
opts := zap.Options{
Development: true,
}
@@ -171,10 +186,29 @@ func main() {
// Shared quota enforcer used by both the webhook validator and TenantQuota reconciler.
quotaEnforcer := quota.NewEnforcer(gpuResourceName)
+ // Build the CanaryController when --prometheus-url is provided.
+ // When nil, AgentDeployments with strategy=Canary behave as Recreate.
+ var canaryController *rollout.Controller
+ if prometheusURL != "" {
+ setupLog.Info("canary rollout enabled", "prometheusURL", prometheusURL,
+ "gatewayName", gatewayName, "gatewayNamespace", gatewayNamespace)
+ canaryController = &rollout.Controller{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ PromClient: metrics.NewClient(prometheusURL),
+ GatewayName: gatewayName,
+ GatewayNamespace: gatewayNamespace,
+ FailSafeTimeout: 60 * time.Second,
+ }
+ } else {
+ setupLog.Info("canary rollout disabled (no --prometheus-url)")
+ }
+
if err = (&controller.AgentDeploymentReconciler{
- Client: mgr.GetClient(),
- Scheme: mgr.GetScheme(),
- GPUResourceName: gpuResourceName,
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ GPUResourceName: gpuResourceName,
+ CanaryController: canaryController,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "AgentDeployment")
os.Exit(1)
diff --git a/config/crd/bases/agentrax.io_agentdeployments.yaml b/config/crd/bases/agentrax.io_agentdeployments.yaml
index 1ffa8c7..1282a25 100644
--- a/config/crd/bases/agentrax.io_agentdeployments.yaml
+++ b/config/crd/bases/agentrax.io_agentdeployments.yaml
@@ -377,6 +377,11 @@ spec:
status:
description: AgentDeploymentStatus defines the observed state of an AgentDeployment.
properties:
+ canaryStepIndex:
+ description: |-
+ CanaryStepIndex is the index of the currently executing rollout step.
+ Persisted so the state machine survives operator restarts.
+ type: integer
canaryVersion:
description: CanaryVersion is the container image tag of the canary
deployment, if one is in progress.
@@ -447,6 +452,12 @@ spec:
description: CurrentReplicas is the number of replicas currently running.
format: int32
type: integer
+ pauseStartedAt:
+ description: |-
+ PauseStartedAt records when the current pause step began.
+ Used to enforce maximum pause extensions and the fail-safe rollback timeout.
+ format: date-time
+ type: string
phase:
description: Phase is the high-level lifecycle phase of this deployment.
enum:
@@ -456,6 +467,12 @@ spec:
- RolloutFailed
- Degraded
type: string
+ promUnreachableSince:
+ description: |-
+ PromUnreachableSince records when Prometheus last became unreachable.
+ When non-nil and age exceeds FailSafeTimeout, a fail-safe rollback fires.
+ format: date-time
+ type: string
registered:
description: Registered is true when this agent is currently registered
in the MCP registry.
diff --git a/config/crd/external/gateway.networking.k8s.io_httproutes.yaml b/config/crd/external/gateway.networking.k8s.io_httproutes.yaml
new file mode 100644
index 0000000..41b296f
--- /dev/null
+++ b/config/crd/external/gateway.networking.k8s.io_httproutes.yaml
@@ -0,0 +1,6046 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/2997
+ gateway.networking.k8s.io/bundle-version: v1.1.0
+ gateway.networking.k8s.io/channel: standard
+ creationTimestamp: null
+ name: httproutes.gateway.networking.k8s.io
+spec:
+ group: gateway.networking.k8s.io
+ names:
+ categories:
+ - gateway-api
+ kind: HTTPRoute
+ listKind: HTTPRouteList
+ plural: httproutes
+ singular: httproute
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.hostnames
+ name: Hostnames
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: |-
+ 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.
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: Spec defines the desired state of HTTPRoute.
+ properties:
+ hostnames:
+ description: |-
+ Hostnames defines a set of hostnames that should match against the HTTP Host
+ header to select a HTTPRoute used to process the request. Implementations
+ MUST ignore any port value specified in the HTTP Host header while
+ performing a match and (absent of any applicable header modification
+ configuration) MUST forward this header unmodified to the backend.
+
+
+ Valid values for Hostnames are determined by 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.
+
+
+ If a hostname is specified by both the Listener and HTTPRoute, there
+ must be at least one intersecting hostname for the HTTPRoute to be
+ attached to the Listener. For example:
+
+
+ * A Listener with `test.example.com` as the hostname matches HTTPRoutes
+ that have either not specified any hostnames, or have specified at
+ least one of `test.example.com` or `*.example.com`.
+ * A Listener with `*.example.com` as the hostname matches HTTPRoutes
+ that have either not specified any hostnames or have specified at least
+ one hostname that matches the Listener hostname. For example,
+ `*.example.com`, `test.example.com`, and `foo.test.example.com` would
+ all match. On the other hand, `example.com` and `test.example.net` would
+ not match.
+
+
+ Hostnames that are prefixed with a wildcard label (`*.`) are interpreted
+ as a suffix match. That means that a match for `*.example.com` would match
+ both `test.example.com`, and `foo.test.example.com`, but not `example.com`.
+
+
+ If both the Listener and HTTPRoute have specified hostnames, any
+ HTTPRoute hostnames that do not match the Listener hostname MUST be
+ ignored. For example, if a Listener specified `*.example.com`, and the
+ HTTPRoute specified `test.example.com` and `test.example.net`,
+ `test.example.net` must not be considered for a match.
+
+
+ If both the Listener and HTTPRoute have specified hostnames, and none
+ match with the criteria above, then the HTTPRoute is not accepted. The
+ implementation must raise an 'Accepted' Condition with a status of
+ `False` in the corresponding RouteParentStatus.
+
+
+ In the event that multiple HTTPRoutes specify intersecting hostnames (e.g.
+ overlapping wildcard matching and exact matching hostnames), precedence must
+ be given to rules from the HTTPRoute with the largest number of:
+
+
+ * Characters in a matching non-wildcard hostname.
+ * Characters in a matching hostname.
+
+
+ If ties exist across multiple Routes, the matching precedence rules for
+ HTTPRouteMatches takes over.
+
+
+ Support: Core
+ items:
+ description: |-
+ Hostname is the fully qualified domain name of a network host. 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.
+
+
+ Hostname can be "precise" which is a domain name without the terminating
+ dot of a network host (e.g. "foo.example.com") or "wildcard", which is a
+ domain name prefixed with a single wildcard label (e.g. `*.example.com`).
+
+
+ Note that as per RFC1035 and RFC1123, a *label* must consist of lower case
+ alphanumeric characters or '-', and must start and end with an alphanumeric
+ character. No other punctuation is allowed.
+ maxLength: 253
+ minLength: 1
+ pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ maxItems: 16
+ type: array
+ parentRefs:
+ description: |+
+ ParentRefs references the resources (usually Gateways) that a Route wants
+ to be attached to. Note that the referenced parent resource needs to
+ allow this for the attachment to be complete. For Gateways, that means
+ the Gateway needs to allow attachment from Routes of this kind and
+ namespace. For Services, that means the Service must either be in the same
+ namespace for a "producer" route, or the mesh implementation must support
+ and allow "consumer" routes for the referenced Service. ReferenceGrant is
+ not applicable for governing ParentRefs to Services - it is not possible to
+ create a "producer" route for a Service in a different namespace from the
+ Route.
+
+
+ There are two kinds of parent resources with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ This API may be extended in the future to support additional kinds of parent
+ resources.
+
+
+ ParentRefs must be _distinct_. This means either that:
+
+
+ * They select different objects. If this is the case, then parentRef
+ entries are distinct. In terms of fields, this means that the
+ multi-part key defined by `group`, `kind`, `namespace`, and `name` must
+ be unique across all parentRef entries in the Route.
+ * They do not select different objects, but for each optional field used,
+ each ParentRef that selects the same object must set the same set of
+ optional fields to different values. If one ParentRef sets a
+ combination of optional fields, all must set the same combination.
+
+
+ Some examples:
+
+
+ * If one ParentRef sets `sectionName`, all ParentRefs referencing the
+ same object must also set `sectionName`.
+ * If one ParentRef sets `port`, all ParentRefs referencing the same
+ object must also set `port`.
+ * If one ParentRef sets `sectionName` and `port`, all ParentRefs
+ referencing the same object must also set `sectionName` and `port`.
+
+
+ It is possible to separately reference multiple distinct objects that may
+ be collapsed by an implementation. For example, some implementations may
+ choose to merge compatible Gateway Listeners together. If that is the
+ case, the list of routes attached to those resources should also be
+ merged.
+
+
+ Note that for ParentRefs that cross namespace boundaries, there are specific
+ rules. Cross-namespace references are only valid if they are explicitly
+ allowed by something in the namespace they are referring to. For example,
+ Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+ generic way to enable other kinds of cross-namespace reference.
+
+
+
+
+
+
+
+
+ items:
+ description: |-
+ ParentReference identifies an API object (usually a Gateway) that can be considered
+ a parent of this resource (usually a route). There are two kinds of parent resources
+ with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ This API may be extended in the future to support additional kinds of parent
+ resources.
+
+
+ The API object must be valid in the cluster; the Group and Kind must
+ be registered in the cluster for this reference to be valid.
+ properties:
+ group:
+ default: gateway.networking.k8s.io
+ description: |-
+ Group is the group of the referent.
+ When unspecified, "gateway.networking.k8s.io" is inferred.
+ To set the core API group (such as for a "Service" kind referent),
+ Group must be explicitly set to "" (empty string).
+
+
+ Support: Core
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Gateway
+ description: |-
+ Kind is kind of the referent.
+
+
+ There are two kinds of parent resources with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ Support for other resources is Implementation-Specific.
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: |-
+ Name is the name of the referent.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the referent. When unspecified, this refers
+ to the local namespace of the Route.
+
+
+ Note that there are specific rules for ParentRefs which cross namespace
+ boundaries. Cross-namespace references are only valid if they are explicitly
+ allowed by something in the namespace they are referring to. For example:
+ Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+ generic way to enable any other kind of cross-namespace reference.
+
+
+
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port is the network port this Route targets. It can be interpreted
+ differently based on the type of parent resource.
+
+
+ When the parent resource is a Gateway, this targets all listeners
+ listening on the specified port that also support this kind of Route(and
+ select this Route). It's not recommended to set `Port` unless the
+ networking behaviors specified in a Route must apply to a specific port
+ as opposed to a listener(s) whose port(s) may be changed. When both Port
+ and SectionName are specified, the name and port of the selected listener
+ must match both specified values.
+
+
+
+
+
+ Implementations MAY choose to support other parent resources.
+ Implementations supporting other types of parent resources MUST clearly
+ document how/if Port is interpreted.
+
+
+ For the purpose of status, an attachment is considered successful as
+ long as the parent resource accepts it partially. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
+ from the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route,
+ the Route MUST be considered detached from the Gateway.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ sectionName:
+ description: |-
+ SectionName is the name of a section within the target resource. In the
+ following resources, SectionName is interpreted as the following:
+
+
+ * Gateway: Listener name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+ * Service: Port name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+
+
+ Implementations MAY choose to support attaching Routes to other resources.
+ If that is the case, they MUST clearly document how SectionName is
+ interpreted.
+
+
+ When unspecified (empty string), this will reference the entire resource.
+ For the purpose of status, an attachment is considered successful if at
+ least one section in the parent resource accepts it. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
+ the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route, the
+ Route MUST be considered detached from the Gateway.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - name
+ type: object
+ maxItems: 32
+ type: array
+ x-kubernetes-validations:
+ - message: sectionName must be specified when parentRefs includes
+ 2 or more references to the same parent
+ rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind
+ == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__)
+ || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__
+ == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) &&
+ p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName)
+ || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName
+ == '''')) : true))'
+ - message: sectionName must be unique when parentRefs includes 2 or
+ more references to the same parent
+ rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind
+ == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__)
+ || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__
+ == '')) || (has(p1.__namespace__) && has(p2.__namespace__) &&
+ p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName)
+ || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName
+ == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName
+ == p2.sectionName))))
+ rules:
+ default:
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /
+ description: Rules are a list of HTTP matchers, filters and actions.
+ items:
+ description: |-
+ HTTPRouteRule defines semantics for matching an HTTP request based on
+ conditions (matches), processing it (filters), and forwarding the request to
+ an API object (backendRefs).
+ properties:
+ backendRefs:
+ description: |-
+ BackendRefs defines the backend(s) where matching requests should be
+ sent.
+
+
+ Failure behavior here depends on how many BackendRefs are specified and
+ how many are invalid.
+
+
+ If *all* entries in BackendRefs are invalid, and there are also no filters
+ specified in this route rule, *all* traffic which matches this rule MUST
+ receive a 500 status code.
+
+
+ See the HTTPBackendRef definition for the rules about what makes a single
+ HTTPBackendRef invalid.
+
+
+ When a HTTPBackendRef is invalid, 500 status codes MUST be returned for
+ requests that would have otherwise been routed to an invalid backend. If
+ multiple backends are specified, and some are invalid, the proportion of
+ requests that would otherwise have been routed to an invalid backend
+ MUST receive a 500 status code.
+
+
+ For example, if two backends are specified with equal weights, and one is
+ invalid, 50 percent of traffic must receive a 500. Implementations may
+ choose how that 50 percent is determined.
+
+
+ Support: Core for Kubernetes Service
+
+
+ Support: Extended for Kubernetes ServiceImport
+
+
+ Support: Implementation-specific for any other resource
+
+
+ Support for weight: Core
+ items:
+ description: |-
+ HTTPBackendRef defines how a HTTPRoute forwards a HTTP request.
+
+
+ Note that when a namespace different than the local namespace is specified, a
+ ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+
+
+
+ When the BackendRef points to a Kubernetes Service, implementations SHOULD
+ honor the appProtocol field if it is set for the target Service Port.
+
+
+ Implementations supporting appProtocol SHOULD recognize the Kubernetes
+ Standard Application Protocols defined in KEP-3726.
+
+
+ If a Service appProtocol isn't specified, an implementation MAY infer the
+ backend protocol through its own means. Implementations MAY infer the
+ protocol from the Route type referring to the backend Service.
+
+
+ If a Route is not able to send traffic to the backend using the specified
+ protocol then the backend is considered invalid. Implementations MUST set the
+ "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason.
+
+
+
+ properties:
+ filters:
+ description: |-
+ Filters defined at this level should be executed if and only if the
+ request is being forwarded to the backend defined here.
+
+
+ Support: Implementation-specific (For broader support of filters, use the
+ Filters field in HTTPRouteRule.)
+ items:
+ description: |-
+ 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.
+ properties:
+ extensionRef:
+ description: |-
+ ExtensionRef is an optional, implementation-specific extension to the
+ "filter" behavior. For example, resource "myroutefilter" in group
+ "networking.example.net"). ExtensionRef MUST NOT be used for core and
+ extended filters.
+
+
+ This filter can be used multiple times within the same rule.
+
+
+ Support: Implementation-specific
+ properties:
+ group:
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ description: Kind is kind of the referent. For
+ example "HTTPRoute" or "Service".
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - group
+ - kind
+ - name
+ type: object
+ requestHeaderModifier:
+ description: |-
+ RequestHeaderModifier defines a schema for a filter that modifies request
+ headers.
+
+
+ Support: Core
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ requestMirror:
+ description: |-
+ RequestMirror defines a schema for a filter that mirrors requests.
+ Requests are sent to the specified destination, but responses from
+ that destination are ignored.
+
+
+ This filter can be used multiple times within the same rule. Note that
+ not all implementations will be able to support mirroring to multiple
+ backends.
+
+
+ Support: Extended
+ properties:
+ backendRef:
+ description: |-
+ BackendRef references a resource where mirrored requests are sent.
+
+
+ Mirrored requests must be sent only to a single destination endpoint
+ within this BackendRef, irrespective of how many endpoints are present
+ within this BackendRef.
+
+
+ If the referent cannot be found, this BackendRef is invalid and must be
+ dropped from the Gateway. The controller must ensure the "ResolvedRefs"
+ condition on the Route status is set to `status: False` and not configure
+ this backend in the underlying implementation.
+
+
+ If there is a cross-namespace reference to an *existing* object
+ that is not allowed by a ReferenceGrant, the controller must ensure the
+ "ResolvedRefs" condition on the Route is set to `status: False`,
+ with the "RefNotPermitted" reason and not configure this backend in the
+ underlying implementation.
+
+
+ In either error case, the Message of the `ResolvedRefs` Condition
+ should be used to provide more detail about the problem.
+
+
+ Support: Extended for Kubernetes Service
+
+
+ Support: Implementation-specific for any other resource
+ properties:
+ group:
+ default: ""
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Service
+ description: |-
+ Kind is the Kubernetes resource kind of the referent. For example
+ "Service".
+
+
+ Defaults to "Service" when not specified.
+
+
+ ExternalName services can refer to CNAME DNS records that may live
+ outside of the cluster and as such are difficult to reason about in
+ terms of conformance. They also may not be safe to forward to (see
+ CVE-2021-25740 for more information). Implementations SHOULD NOT
+ support ExternalName Services.
+
+
+ Support: Core (Services with a type other than ExternalName)
+
+
+ Support: Implementation-specific (Services with type ExternalName)
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the backend. When unspecified, the local
+ namespace is inferred.
+
+
+ Note that when a namespace different than the local namespace is specified,
+ a ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port specifies the destination port number to use for this resource.
+ Port is required when the referent is a Kubernetes Service. In this
+ case, the port number is the service port number, not the target port.
+ For other resources, destination port might be derived from the referent
+ resource or this field.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must have port for Service reference
+ rule: '(size(self.group) == 0 && self.kind
+ == ''Service'') ? has(self.port) : true'
+ required:
+ - backendRef
+ type: object
+ requestRedirect:
+ description: |-
+ RequestRedirect defines a schema for a filter that responds to the
+ request with an HTTP redirection.
+
+
+ Support: Core
+ properties:
+ hostname:
+ description: |-
+ Hostname is the hostname to be used in the value of the `Location`
+ header in the response.
+ When empty, the hostname in the `Host` header of the request is used.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines parameters used to modify the path of the incoming request.
+ The modified path is then used to construct the `Location` header. When
+ empty, the request path is used as-is.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified
+ when type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ?
+ has(self.replaceFullPath) : true'
+ - message: type must be 'ReplaceFullPath' when
+ replaceFullPath is set
+ rule: 'has(self.replaceFullPath) ? self.type
+ == ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified
+ when type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch''
+ ? has(self.replacePrefixMatch) : true'
+ - message: type must be 'ReplacePrefixMatch'
+ when replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ port:
+ description: |-
+ Port is the port to be used in the value of the `Location`
+ header in the response.
+
+
+ If no port is specified, the redirect port MUST be derived using the
+ following rules:
+
+
+ * If redirect scheme is not-empty, the redirect port MUST be the well-known
+ port associated with the redirect scheme. Specifically "http" to port 80
+ and "https" to port 443. If the redirect scheme does not have a
+ well-known port, the listener port of the Gateway SHOULD be used.
+ * If redirect scheme is empty, the redirect port MUST be the Gateway
+ Listener port.
+
+
+ Implementations SHOULD NOT add the port number in the 'Location'
+ header in the following cases:
+
+
+ * A Location header that will use HTTP (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 80.
+ * A Location header that will use HTTPS (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 443.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ scheme:
+ description: |-
+ Scheme is the scheme to be used in the value of the `Location` header in
+ the response. When empty, the scheme of the request is used.
+
+
+ Scheme redirects can affect the port of the redirect, for more information,
+ refer to the documentation for the port field of this filter.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Extended
+ enum:
+ - http
+ - https
+ type: string
+ statusCode:
+ default: 302
+ description: |-
+ StatusCode is the HTTP status code to be used in response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Core
+ enum:
+ - 301
+ - 302
+ type: integer
+ type: object
+ responseHeaderModifier:
+ description: |-
+ ResponseHeaderModifier defines a schema for a filter that modifies response
+ headers.
+
+
+ Support: Extended
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ type:
+ description: |-
+ Type identifies the type of filter to apply. As with other API fields,
+ types are classified into three conformance levels:
+
+
+ - Core: Filter types and their corresponding configuration defined by
+ "Support: Core" in this package, e.g. "RequestHeaderModifier". All
+ implementations must support core filters.
+
+
+ - Extended: Filter types and their corresponding configuration defined by
+ "Support: Extended" in this package, e.g. "RequestMirror". Implementers
+ are encouraged to support extended filters.
+
+
+ - Implementation-specific: Filters that are defined and supported by
+ specific vendors.
+ In the future, filters showing convergence in behavior across multiple
+ implementations will be considered for inclusion in extended or core
+ conformance levels. Filter-specific configuration for such filters
+ is specified using the ExtensionRef field. `Type` should be set to
+ "ExtensionRef" for custom filters.
+
+
+ Implementers are encouraged to define custom implementation types to
+ extend the core API with implementation-specific behavior.
+
+
+ If a reference to a custom filter type cannot be resolved, the filter
+ MUST NOT be skipped. Instead, requests that would have been processed by
+ that filter MUST receive a HTTP error response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - RequestHeaderModifier
+ - ResponseHeaderModifier
+ - RequestMirror
+ - RequestRedirect
+ - URLRewrite
+ - ExtensionRef
+ type: string
+ urlRewrite:
+ description: |-
+ URLRewrite defines a schema for a filter that modifies a request during forwarding.
+
+
+ Support: Extended
+ properties:
+ hostname:
+ description: |-
+ Hostname is the value to be used to replace the Host header value during
+ forwarding.
+
+
+ Support: Extended
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines a path rewrite.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified
+ when type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ?
+ has(self.replaceFullPath) : true'
+ - message: type must be 'ReplaceFullPath' when
+ replaceFullPath is set
+ rule: 'has(self.replaceFullPath) ? self.type
+ == ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified
+ when type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch''
+ ? has(self.replacePrefixMatch) : true'
+ - message: type must be 'ReplacePrefixMatch'
+ when replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ type: object
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: filter.requestHeaderModifier must be nil
+ if the filter.type is not RequestHeaderModifier
+ rule: '!(has(self.requestHeaderModifier) && self.type
+ != ''RequestHeaderModifier'')'
+ - message: filter.requestHeaderModifier must be specified
+ for RequestHeaderModifier filter.type
+ rule: '!(!has(self.requestHeaderModifier) && self.type
+ == ''RequestHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be nil
+ if the filter.type is not ResponseHeaderModifier
+ rule: '!(has(self.responseHeaderModifier) && self.type
+ != ''ResponseHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be specified
+ for ResponseHeaderModifier filter.type
+ rule: '!(!has(self.responseHeaderModifier) && self.type
+ == ''ResponseHeaderModifier'')'
+ - message: filter.requestMirror must be nil if the filter.type
+ is not RequestMirror
+ rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')'
+ - message: filter.requestMirror must be specified for
+ RequestMirror filter.type
+ rule: '!(!has(self.requestMirror) && self.type ==
+ ''RequestMirror'')'
+ - message: filter.requestRedirect must be nil if the
+ filter.type is not RequestRedirect
+ rule: '!(has(self.requestRedirect) && self.type !=
+ ''RequestRedirect'')'
+ - message: filter.requestRedirect must be specified
+ for RequestRedirect filter.type
+ rule: '!(!has(self.requestRedirect) && self.type ==
+ ''RequestRedirect'')'
+ - message: filter.urlRewrite must be nil if the filter.type
+ is not URLRewrite
+ rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')'
+ - message: filter.urlRewrite must be specified for URLRewrite
+ filter.type
+ rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')'
+ - message: filter.extensionRef must be nil if the filter.type
+ is not ExtensionRef
+ rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')'
+ - message: filter.extensionRef must be specified for
+ ExtensionRef filter.type
+ rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')'
+ maxItems: 16
+ type: array
+ x-kubernetes-validations:
+ - message: May specify either httpRouteFilterRequestRedirect
+ or httpRouteFilterRequestRewrite, but not both
+ rule: '!(self.exists(f, f.type == ''RequestRedirect'')
+ && self.exists(f, f.type == ''URLRewrite''))'
+ - message: May specify either httpRouteFilterRequestRedirect
+ or httpRouteFilterRequestRewrite, but not both
+ rule: '!(self.exists(f, f.type == ''RequestRedirect'')
+ && self.exists(f, f.type == ''URLRewrite''))'
+ - message: RequestHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestHeaderModifier').size()
+ <= 1
+ - message: ResponseHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'ResponseHeaderModifier').size()
+ <= 1
+ - message: RequestRedirect filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestRedirect').size()
+ <= 1
+ - message: URLRewrite filter cannot be repeated
+ rule: self.filter(f, f.type == 'URLRewrite').size()
+ <= 1
+ group:
+ default: ""
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Service
+ description: |-
+ Kind is the Kubernetes resource kind of the referent. For example
+ "Service".
+
+
+ Defaults to "Service" when not specified.
+
+
+ ExternalName services can refer to CNAME DNS records that may live
+ outside of the cluster and as such are difficult to reason about in
+ terms of conformance. They also may not be safe to forward to (see
+ CVE-2021-25740 for more information). Implementations SHOULD NOT
+ support ExternalName Services.
+
+
+ Support: Core (Services with a type other than ExternalName)
+
+
+ Support: Implementation-specific (Services with type ExternalName)
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the backend. When unspecified, the local
+ namespace is inferred.
+
+
+ Note that when a namespace different than the local namespace is specified,
+ a ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port specifies the destination port number to use for this resource.
+ Port is required when the referent is a Kubernetes Service. In this
+ case, the port number is the service port number, not the target port.
+ For other resources, destination port might be derived from the referent
+ resource or this field.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ weight:
+ default: 1
+ description: |-
+ Weight specifies the proportion of requests forwarded to the referenced
+ backend. This is computed as weight/(sum of all weights in this
+ BackendRefs list). For non-zero values, there may be some epsilon from
+ the exact proportion defined here depending on the precision an
+ implementation supports. Weight is not a percentage and the sum of
+ weights does not need to equal 100.
+
+
+ If only one backend is specified and it has a weight greater than 0, 100%
+ of the traffic is forwarded to that backend. If weight is set to 0, no
+ traffic should be forwarded for this entry. If unspecified, weight
+ defaults to 1.
+
+
+ Support for this field varies based on the context where used.
+ format: int32
+ maximum: 1000000
+ minimum: 0
+ type: integer
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must have port for Service reference
+ rule: '(size(self.group) == 0 && self.kind == ''Service'')
+ ? has(self.port) : true'
+ maxItems: 16
+ type: array
+ filters:
+ description: |-
+ Filters define the filters that are applied to requests that match
+ this rule.
+
+
+ Wherever possible, implementations SHOULD implement filters in the order
+ they are specified.
+
+
+ Implementations MAY choose to implement this ordering strictly, rejecting
+ any combination or order of filters that can not be supported. If implementations
+ choose a strict interpretation of filter ordering, they MUST clearly document
+ that behavior.
+
+
+ To reject an invalid combination or order of filters, implementations SHOULD
+ consider the Route Rules with this configuration invalid. If all Route Rules
+ in a Route are invalid, the entire Route would be considered invalid. If only
+ a portion of Route Rules are invalid, implementations MUST set the
+ "PartiallyInvalid" condition for the Route.
+
+
+ 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 the same filter multiple times is not supported unless explicitly
+ indicated in the filter.
+
+
+ All filters are expected to be compatible with each other except for the
+ URLRewrite and RequestRedirect filters, which may not be combined. If an
+ implementation can not support other combinations of filters, they must clearly
+ document that limitation. In cases where incompatible or unsupported
+ filters are specified and cause the `Accepted` condition to be set to status
+ `False`, implementations may use the `IncompatibleFilters` reason to specify
+ this configuration error.
+
+
+ Support: Core
+ items:
+ description: |-
+ 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.
+ properties:
+ extensionRef:
+ description: |-
+ ExtensionRef is an optional, implementation-specific extension to the
+ "filter" behavior. For example, resource "myroutefilter" in group
+ "networking.example.net"). ExtensionRef MUST NOT be used for core and
+ extended filters.
+
+
+ This filter can be used multiple times within the same rule.
+
+
+ Support: Implementation-specific
+ properties:
+ group:
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ description: Kind is kind of the referent. For example
+ "HTTPRoute" or "Service".
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - group
+ - kind
+ - name
+ type: object
+ requestHeaderModifier:
+ description: |-
+ RequestHeaderModifier defines a schema for a filter that modifies request
+ headers.
+
+
+ Support: Core
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ requestMirror:
+ description: |-
+ RequestMirror defines a schema for a filter that mirrors requests.
+ Requests are sent to the specified destination, but responses from
+ that destination are ignored.
+
+
+ This filter can be used multiple times within the same rule. Note that
+ not all implementations will be able to support mirroring to multiple
+ backends.
+
+
+ Support: Extended
+ properties:
+ backendRef:
+ description: |-
+ BackendRef references a resource where mirrored requests are sent.
+
+
+ Mirrored requests must be sent only to a single destination endpoint
+ within this BackendRef, irrespective of how many endpoints are present
+ within this BackendRef.
+
+
+ If the referent cannot be found, this BackendRef is invalid and must be
+ dropped from the Gateway. The controller must ensure the "ResolvedRefs"
+ condition on the Route status is set to `status: False` and not configure
+ this backend in the underlying implementation.
+
+
+ If there is a cross-namespace reference to an *existing* object
+ that is not allowed by a ReferenceGrant, the controller must ensure the
+ "ResolvedRefs" condition on the Route is set to `status: False`,
+ with the "RefNotPermitted" reason and not configure this backend in the
+ underlying implementation.
+
+
+ In either error case, the Message of the `ResolvedRefs` Condition
+ should be used to provide more detail about the problem.
+
+
+ Support: Extended for Kubernetes Service
+
+
+ Support: Implementation-specific for any other resource
+ properties:
+ group:
+ default: ""
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Service
+ description: |-
+ Kind is the Kubernetes resource kind of the referent. For example
+ "Service".
+
+
+ Defaults to "Service" when not specified.
+
+
+ ExternalName services can refer to CNAME DNS records that may live
+ outside of the cluster and as such are difficult to reason about in
+ terms of conformance. They also may not be safe to forward to (see
+ CVE-2021-25740 for more information). Implementations SHOULD NOT
+ support ExternalName Services.
+
+
+ Support: Core (Services with a type other than ExternalName)
+
+
+ Support: Implementation-specific (Services with type ExternalName)
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the backend. When unspecified, the local
+ namespace is inferred.
+
+
+ Note that when a namespace different than the local namespace is specified,
+ a ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port specifies the destination port number to use for this resource.
+ Port is required when the referent is a Kubernetes Service. In this
+ case, the port number is the service port number, not the target port.
+ For other resources, destination port might be derived from the referent
+ resource or this field.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must have port for Service reference
+ rule: '(size(self.group) == 0 && self.kind == ''Service'')
+ ? has(self.port) : true'
+ required:
+ - backendRef
+ type: object
+ requestRedirect:
+ description: |-
+ RequestRedirect defines a schema for a filter that responds to the
+ request with an HTTP redirection.
+
+
+ Support: Core
+ properties:
+ hostname:
+ description: |-
+ Hostname is the hostname to be used in the value of the `Location`
+ header in the response.
+ When empty, the hostname in the `Host` header of the request is used.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines parameters used to modify the path of the incoming request.
+ The modified path is then used to construct the `Location` header. When
+ empty, the request path is used as-is.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified when
+ type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath)
+ : true'
+ - message: type must be 'ReplaceFullPath' when replaceFullPath
+ is set
+ rule: 'has(self.replaceFullPath) ? self.type ==
+ ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified when
+ type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch)
+ : true'
+ - message: type must be 'ReplacePrefixMatch' when
+ replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ port:
+ description: |-
+ Port is the port to be used in the value of the `Location`
+ header in the response.
+
+
+ If no port is specified, the redirect port MUST be derived using the
+ following rules:
+
+
+ * If redirect scheme is not-empty, the redirect port MUST be the well-known
+ port associated with the redirect scheme. Specifically "http" to port 80
+ and "https" to port 443. If the redirect scheme does not have a
+ well-known port, the listener port of the Gateway SHOULD be used.
+ * If redirect scheme is empty, the redirect port MUST be the Gateway
+ Listener port.
+
+
+ Implementations SHOULD NOT add the port number in the 'Location'
+ header in the following cases:
+
+
+ * A Location header that will use HTTP (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 80.
+ * A Location header that will use HTTPS (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 443.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ scheme:
+ description: |-
+ Scheme is the scheme to be used in the value of the `Location` header in
+ the response. When empty, the scheme of the request is used.
+
+
+ Scheme redirects can affect the port of the redirect, for more information,
+ refer to the documentation for the port field of this filter.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Extended
+ enum:
+ - http
+ - https
+ type: string
+ statusCode:
+ default: 302
+ description: |-
+ StatusCode is the HTTP status code to be used in response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Core
+ enum:
+ - 301
+ - 302
+ type: integer
+ type: object
+ responseHeaderModifier:
+ description: |-
+ ResponseHeaderModifier defines a schema for a filter that modifies response
+ headers.
+
+
+ Support: Extended
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ type:
+ description: |-
+ Type identifies the type of filter to apply. As with other API fields,
+ types are classified into three conformance levels:
+
+
+ - Core: Filter types and their corresponding configuration defined by
+ "Support: Core" in this package, e.g. "RequestHeaderModifier". All
+ implementations must support core filters.
+
+
+ - Extended: Filter types and their corresponding configuration defined by
+ "Support: Extended" in this package, e.g. "RequestMirror". Implementers
+ are encouraged to support extended filters.
+
+
+ - Implementation-specific: Filters that are defined and supported by
+ specific vendors.
+ In the future, filters showing convergence in behavior across multiple
+ implementations will be considered for inclusion in extended or core
+ conformance levels. Filter-specific configuration for such filters
+ is specified using the ExtensionRef field. `Type` should be set to
+ "ExtensionRef" for custom filters.
+
+
+ Implementers are encouraged to define custom implementation types to
+ extend the core API with implementation-specific behavior.
+
+
+ If a reference to a custom filter type cannot be resolved, the filter
+ MUST NOT be skipped. Instead, requests that would have been processed by
+ that filter MUST receive a HTTP error response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - RequestHeaderModifier
+ - ResponseHeaderModifier
+ - RequestMirror
+ - RequestRedirect
+ - URLRewrite
+ - ExtensionRef
+ type: string
+ urlRewrite:
+ description: |-
+ URLRewrite defines a schema for a filter that modifies a request during forwarding.
+
+
+ Support: Extended
+ properties:
+ hostname:
+ description: |-
+ Hostname is the value to be used to replace the Host header value during
+ forwarding.
+
+
+ Support: Extended
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines a path rewrite.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified when
+ type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath)
+ : true'
+ - message: type must be 'ReplaceFullPath' when replaceFullPath
+ is set
+ rule: 'has(self.replaceFullPath) ? self.type ==
+ ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified when
+ type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch)
+ : true'
+ - message: type must be 'ReplacePrefixMatch' when
+ replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ type: object
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: filter.requestHeaderModifier must be nil if the
+ filter.type is not RequestHeaderModifier
+ rule: '!(has(self.requestHeaderModifier) && self.type !=
+ ''RequestHeaderModifier'')'
+ - message: filter.requestHeaderModifier must be specified
+ for RequestHeaderModifier filter.type
+ rule: '!(!has(self.requestHeaderModifier) && self.type ==
+ ''RequestHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be nil if the
+ filter.type is not ResponseHeaderModifier
+ rule: '!(has(self.responseHeaderModifier) && self.type !=
+ ''ResponseHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be specified
+ for ResponseHeaderModifier filter.type
+ rule: '!(!has(self.responseHeaderModifier) && self.type
+ == ''ResponseHeaderModifier'')'
+ - message: filter.requestMirror must be nil if the filter.type
+ is not RequestMirror
+ rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')'
+ - message: filter.requestMirror must be specified for RequestMirror
+ filter.type
+ rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')'
+ - message: filter.requestRedirect must be nil if the filter.type
+ is not RequestRedirect
+ rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')'
+ - message: filter.requestRedirect must be specified for RequestRedirect
+ filter.type
+ rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')'
+ - message: filter.urlRewrite must be nil if the filter.type
+ is not URLRewrite
+ rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')'
+ - message: filter.urlRewrite must be specified for URLRewrite
+ filter.type
+ rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')'
+ - message: filter.extensionRef must be nil if the filter.type
+ is not ExtensionRef
+ rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')'
+ - message: filter.extensionRef must be specified for ExtensionRef
+ filter.type
+ rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')'
+ maxItems: 16
+ type: array
+ x-kubernetes-validations:
+ - message: May specify either httpRouteFilterRequestRedirect
+ or httpRouteFilterRequestRewrite, but not both
+ rule: '!(self.exists(f, f.type == ''RequestRedirect'') &&
+ self.exists(f, f.type == ''URLRewrite''))'
+ - message: RequestHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestHeaderModifier').size()
+ <= 1
+ - message: ResponseHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'ResponseHeaderModifier').size()
+ <= 1
+ - message: RequestRedirect filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestRedirect').size() <=
+ 1
+ - message: URLRewrite filter cannot be repeated
+ rule: self.filter(f, f.type == 'URLRewrite').size() <= 1
+ matches:
+ default:
+ - path:
+ type: PathPrefix
+ value: /
+ description: |-
+ 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:
+
+
+ ```
+ 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 matches based on the following criteria, continuing on
+ ties. Across all rules specified on applicable Routes, precedence must be
+ given to the match having:
+
+
+ * "Exact" path match.
+ * "Prefix" path match with largest number of characters.
+ * Method match.
+ * Largest number of header matches.
+ * Largest number of query param matches.
+
+
+ Note: The precedence of RegularExpression path matches are implementation-specific.
+
+
+ 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 an HTTPRoute, matching precedence MUST be granted
+ to the FIRST matching rule (in list order) with a match 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.
+ items:
+ description: "HTTPRouteMatch defines the predicate used to
+ match requests to a given\naction. Multiple match types
+ are ANDed together, i.e. the match will\nevaluate to true
+ only if all conditions are satisfied.\n\n\nFor example,
+ the match below will match a HTTP request only if its path\nstarts
+ with `/foo` AND it contains the `version: v1` header:\n\n\n```\nmatch:\n\n\n\tpath:\n\t
+ \ value: \"/foo\"\n\theaders:\n\t- name: \"version\"\n\t
+ \ value \"v1\"\n\n\n```"
+ properties:
+ headers:
+ description: |-
+ Headers specifies HTTP request header matchers. Multiple match values are
+ ANDed together, meaning, a request must match all the specified headers
+ to select the route.
+ items:
+ description: |-
+ HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request
+ headers.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, only the first
+ entry with an equivalent name MUST be considered for a match. Subsequent
+ entries with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+
+
+ When a header is repeated in an HTTP request, it is
+ implementation-specific behavior as to how this is represented.
+ Generally, proxies should follow the guidance from the RFC:
+ https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 regarding
+ processing a repeated header, with special handling for "Set-Cookie".
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ type:
+ default: Exact
+ description: |-
+ Type specifies how to match against the value of the header.
+
+
+ Support: Core (Exact)
+
+
+ Support: Implementation-specific (RegularExpression)
+
+
+ Since RegularExpression HeaderMatchType has implementation-specific
+ conformance, implementations can support POSIX, PCRE or any other dialects
+ of regular expressions. Please read the implementation's documentation to
+ determine the supported dialect.
+ enum:
+ - Exact
+ - RegularExpression
+ type: string
+ value:
+ description: Value is the value of HTTP Header to
+ be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ method:
+ description: |-
+ Method specifies HTTP method matcher.
+ When specified, this route will be matched only if the request has the
+ specified method.
+
+
+ Support: Extended
+ enum:
+ - GET
+ - HEAD
+ - POST
+ - PUT
+ - DELETE
+ - CONNECT
+ - OPTIONS
+ - TRACE
+ - PATCH
+ type: string
+ path:
+ default:
+ type: PathPrefix
+ value: /
+ description: |-
+ Path specifies a HTTP request path matcher. If this field is not
+ specified, a default prefix match on the "/" path is provided.
+ properties:
+ type:
+ default: PathPrefix
+ description: |-
+ Type specifies how to match against the path Value.
+
+
+ Support: Core (Exact, PathPrefix)
+
+
+ Support: Implementation-specific (RegularExpression)
+ enum:
+ - Exact
+ - PathPrefix
+ - RegularExpression
+ type: string
+ value:
+ default: /
+ description: Value of the HTTP path to match against.
+ maxLength: 1024
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: value must be an absolute path and start with
+ '/' when type one of ['Exact', 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.startsWith(''/'')
+ : true'
+ - message: must not contain '//' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''//'')
+ : true'
+ - message: must not contain '/./' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/./'')
+ : true'
+ - message: must not contain '/../' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/../'')
+ : true'
+ - message: must not contain '%2f' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2f'')
+ : true'
+ - message: must not contain '%2F' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2F'')
+ : true'
+ - message: must not contain '#' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''#'')
+ : true'
+ - message: must not end with '/..' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/..'')
+ : true'
+ - message: must not end with '/.' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/.'')
+ : true'
+ - message: type must be one of ['Exact', 'PathPrefix',
+ 'RegularExpression']
+ rule: self.type in ['Exact','PathPrefix'] || self.type
+ == 'RegularExpression'
+ - message: must only contain valid characters (matching
+ ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$)
+ for types ['Exact', 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""")
+ : true'
+ queryParams:
+ description: |-
+ QueryParams specifies HTTP query parameter matchers. Multiple match
+ values are ANDed together, meaning, a request must match all the
+ specified query parameters to select the route.
+
+
+ Support: Extended
+ items:
+ description: |-
+ HTTPQueryParamMatch describes how to select a HTTP route by matching HTTP
+ query parameters.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP query param to be matched. This must be an
+ exact string match. (See
+ https://tools.ietf.org/html/rfc7230#section-2.7.3).
+
+
+ If multiple entries specify equivalent query param names, only the first
+ entry with an equivalent name MUST be considered for a match. Subsequent
+ entries with an equivalent query param name MUST be ignored.
+
+
+ If a query param is repeated in an HTTP request, the behavior is
+ purposely left undefined, since different data planes have different
+ capabilities. However, it is *recommended* that implementations should
+ match against the first value of the param if the data plane supports it,
+ as this behavior is expected in other load balancing contexts outside of
+ the Gateway API.
+
+
+ Users SHOULD NOT route traffic based on repeated query params to guard
+ themselves against potential differences in the implementations.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ type:
+ default: Exact
+ description: |-
+ Type specifies how to match against the value of the query parameter.
+
+
+ Support: Extended (Exact)
+
+
+ Support: Implementation-specific (RegularExpression)
+
+
+ Since RegularExpression QueryParamMatchType has Implementation-specific
+ conformance, implementations can support POSIX, PCRE or any other
+ dialects of regular expressions. Please read the implementation's
+ documentation to determine the supported dialect.
+ enum:
+ - Exact
+ - RegularExpression
+ type: string
+ value:
+ description: Value is the value of HTTP query param
+ to be matched.
+ maxLength: 1024
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ maxItems: 8
+ type: array
+ type: object
+ x-kubernetes-validations:
+ - message: RequestRedirect filter must not be used together with
+ backendRefs
+ rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ?
+ (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))):
+ true'
+ - message: When using RequestRedirect filter with path.replacePrefixMatch,
+ exactly one PathPrefix match must be specified
+ rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect)
+ && has(f.requestRedirect.path) && f.requestRedirect.path.type
+ == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch)))
+ ? ((size(self.matches) != 1 || !has(self.matches[0].path) ||
+ self.matches[0].path.type != ''PathPrefix'') ? false : true)
+ : true'
+ - message: When using URLRewrite filter with path.replacePrefixMatch,
+ exactly one PathPrefix match must be specified
+ rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite)
+ && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch''
+ && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches)
+ != 1 || !has(self.matches[0].path) || self.matches[0].path.type
+ != ''PathPrefix'') ? false : true) : true'
+ - message: Within backendRefs, when using RequestRedirect filter
+ with path.replacePrefixMatch, exactly one PathPrefix match must
+ be specified
+ rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b,
+ (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect)
+ && has(f.requestRedirect.path) && f.requestRedirect.path.type
+ == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch)))
+ )) ? ((size(self.matches) != 1 || !has(self.matches[0].path)
+ || self.matches[0].path.type != ''PathPrefix'') ? false : true)
+ : true'
+ - message: Within backendRefs, When using URLRewrite filter with
+ path.replacePrefixMatch, exactly one PathPrefix match must be
+ specified
+ rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b,
+ (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite)
+ && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch''
+ && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches)
+ != 1 || !has(self.matches[0].path) || self.matches[0].path.type
+ != ''PathPrefix'') ? false : true) : true'
+ maxItems: 16
+ type: array
+ type: object
+ status:
+ description: Status defines the current state of HTTPRoute.
+ properties:
+ parents:
+ description: |-
+ Parents is a list of parent resources (usually Gateways) that are
+ associated with the route, and the status of the route with respect to
+ each parent. When this route attaches to a parent, the controller that
+ manages the parent must add an entry to this list when the controller
+ first sees the route and should update the entry as appropriate when the
+ route or gateway is modified.
+
+
+ Note that parent references that cannot be resolved by an implementation
+ of this API will not be added to this list. Implementations of this API
+ can only populate Route status for the Gateways/parent resources they are
+ responsible for.
+
+
+ A maximum of 32 Gateways will be represented in this list. An empty list
+ means the route has not been attached to any Gateway.
+ items:
+ description: |-
+ RouteParentStatus describes the status of a route with respect to an
+ associated Parent.
+ properties:
+ conditions:
+ description: |-
+ Conditions describes the status of the route with respect to the Gateway.
+ Note that the route's availability is also subject to the Gateway's own
+ status conditions and listener status.
+
+
+ If the Route's ParentRef specifies an existing Gateway that supports
+ Routes of this kind AND that Gateway's controller has sufficient access,
+ then that Gateway's controller MUST set the "Accepted" condition on the
+ Route, to indicate whether the route has been accepted or rejected by the
+ Gateway, and why.
+
+
+ A Route MUST be considered "Accepted" if at least one of the Route's
+ rules is implemented by the Gateway.
+
+
+ There are a number of cases where the "Accepted" condition may not be set
+ due to lack of controller visibility, that includes when:
+
+
+ * The Route refers to a non-existent parent.
+ * The Route is of a type that the controller does not support.
+ * The Route is in a namespace the controller does not have access to.
+ items:
+ description: "Condition contains details for one aspect of
+ the current state of this API Resource.\n---\nThis struct
+ is intended for direct use as an array at the field path
+ .status.conditions. For example,\n\n\n\ttype FooStatus
+ struct{\n\t // Represents the observations of a foo's
+ current state.\n\t // Known .status.conditions.type are:
+ \"Available\", \"Progressing\", and \"Degraded\"\n\t //
+ +patchMergeKey=type\n\t // +patchStrategy=merge\n\t //
+ +listType=map\n\t // +listMapKey=type\n\t Conditions
+ []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\"
+ patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+ \ // other fields\n\t}"
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False,
+ Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: |-
+ type of condition in CamelCase or in foo.example.com/CamelCase.
+ ---
+ Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+ useful (see .node.status.conditions), the ability to deconflict is important.
+ The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ maxItems: 8
+ minItems: 1
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ controllerName:
+ description: |-
+ ControllerName is a domain/path string that indicates the name of the
+ controller that wrote this status. This corresponds with the
+ controllerName field on GatewayClass.
+
+
+ Example: "example.net/gateway-controller".
+
+
+ The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
+ valid Kubernetes names
+ (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+
+
+ Controllers MUST populate this field when writing status. Controllers should ensure that
+ entries to status populated with their ControllerName are cleaned up when they are no
+ longer necessary.
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+ type: string
+ parentRef:
+ description: |-
+ ParentRef corresponds with a ParentRef in the spec that this
+ RouteParentStatus struct describes the status of.
+ properties:
+ group:
+ default: gateway.networking.k8s.io
+ description: |-
+ Group is the group of the referent.
+ When unspecified, "gateway.networking.k8s.io" is inferred.
+ To set the core API group (such as for a "Service" kind referent),
+ Group must be explicitly set to "" (empty string).
+
+
+ Support: Core
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Gateway
+ description: |-
+ Kind is kind of the referent.
+
+
+ There are two kinds of parent resources with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ Support for other resources is Implementation-Specific.
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: |-
+ Name is the name of the referent.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the referent. When unspecified, this refers
+ to the local namespace of the Route.
+
+
+ Note that there are specific rules for ParentRefs which cross namespace
+ boundaries. Cross-namespace references are only valid if they are explicitly
+ allowed by something in the namespace they are referring to. For example:
+ Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+ generic way to enable any other kind of cross-namespace reference.
+
+
+
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port is the network port this Route targets. It can be interpreted
+ differently based on the type of parent resource.
+
+
+ When the parent resource is a Gateway, this targets all listeners
+ listening on the specified port that also support this kind of Route(and
+ select this Route). It's not recommended to set `Port` unless the
+ networking behaviors specified in a Route must apply to a specific port
+ as opposed to a listener(s) whose port(s) may be changed. When both Port
+ and SectionName are specified, the name and port of the selected listener
+ must match both specified values.
+
+
+
+
+
+ Implementations MAY choose to support other parent resources.
+ Implementations supporting other types of parent resources MUST clearly
+ document how/if Port is interpreted.
+
+
+ For the purpose of status, an attachment is considered successful as
+ long as the parent resource accepts it partially. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
+ from the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route,
+ the Route MUST be considered detached from the Gateway.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ sectionName:
+ description: |-
+ SectionName is the name of a section within the target resource. In the
+ following resources, SectionName is interpreted as the following:
+
+
+ * Gateway: Listener name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+ * Service: Port name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+
+
+ Implementations MAY choose to support attaching Routes to other resources.
+ If that is the case, they MUST clearly document how SectionName is
+ interpreted.
+
+
+ When unspecified (empty string), this will reference the entire resource.
+ For the purpose of status, an attachment is considered successful if at
+ least one section in the parent resource accepts it. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
+ the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route, the
+ Route MUST be considered detached from the Gateway.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - name
+ type: object
+ required:
+ - controllerName
+ - parentRef
+ type: object
+ maxItems: 32
+ type: array
+ required:
+ - parents
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ - additionalPrinterColumns:
+ - jsonPath: .spec.hostnames
+ name: Hostnames
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1beta1
+ schema:
+ openAPIV3Schema:
+ description: |-
+ 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.
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: Spec defines the desired state of HTTPRoute.
+ properties:
+ hostnames:
+ description: |-
+ Hostnames defines a set of hostnames that should match against the HTTP Host
+ header to select a HTTPRoute used to process the request. Implementations
+ MUST ignore any port value specified in the HTTP Host header while
+ performing a match and (absent of any applicable header modification
+ configuration) MUST forward this header unmodified to the backend.
+
+
+ Valid values for Hostnames are determined by 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.
+
+
+ If a hostname is specified by both the Listener and HTTPRoute, there
+ must be at least one intersecting hostname for the HTTPRoute to be
+ attached to the Listener. For example:
+
+
+ * A Listener with `test.example.com` as the hostname matches HTTPRoutes
+ that have either not specified any hostnames, or have specified at
+ least one of `test.example.com` or `*.example.com`.
+ * A Listener with `*.example.com` as the hostname matches HTTPRoutes
+ that have either not specified any hostnames or have specified at least
+ one hostname that matches the Listener hostname. For example,
+ `*.example.com`, `test.example.com`, and `foo.test.example.com` would
+ all match. On the other hand, `example.com` and `test.example.net` would
+ not match.
+
+
+ Hostnames that are prefixed with a wildcard label (`*.`) are interpreted
+ as a suffix match. That means that a match for `*.example.com` would match
+ both `test.example.com`, and `foo.test.example.com`, but not `example.com`.
+
+
+ If both the Listener and HTTPRoute have specified hostnames, any
+ HTTPRoute hostnames that do not match the Listener hostname MUST be
+ ignored. For example, if a Listener specified `*.example.com`, and the
+ HTTPRoute specified `test.example.com` and `test.example.net`,
+ `test.example.net` must not be considered for a match.
+
+
+ If both the Listener and HTTPRoute have specified hostnames, and none
+ match with the criteria above, then the HTTPRoute is not accepted. The
+ implementation must raise an 'Accepted' Condition with a status of
+ `False` in the corresponding RouteParentStatus.
+
+
+ In the event that multiple HTTPRoutes specify intersecting hostnames (e.g.
+ overlapping wildcard matching and exact matching hostnames), precedence must
+ be given to rules from the HTTPRoute with the largest number of:
+
+
+ * Characters in a matching non-wildcard hostname.
+ * Characters in a matching hostname.
+
+
+ If ties exist across multiple Routes, the matching precedence rules for
+ HTTPRouteMatches takes over.
+
+
+ Support: Core
+ items:
+ description: |-
+ Hostname is the fully qualified domain name of a network host. 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.
+
+
+ Hostname can be "precise" which is a domain name without the terminating
+ dot of a network host (e.g. "foo.example.com") or "wildcard", which is a
+ domain name prefixed with a single wildcard label (e.g. `*.example.com`).
+
+
+ Note that as per RFC1035 and RFC1123, a *label* must consist of lower case
+ alphanumeric characters or '-', and must start and end with an alphanumeric
+ character. No other punctuation is allowed.
+ maxLength: 253
+ minLength: 1
+ pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ maxItems: 16
+ type: array
+ parentRefs:
+ description: |+
+ ParentRefs references the resources (usually Gateways) that a Route wants
+ to be attached to. Note that the referenced parent resource needs to
+ allow this for the attachment to be complete. For Gateways, that means
+ the Gateway needs to allow attachment from Routes of this kind and
+ namespace. For Services, that means the Service must either be in the same
+ namespace for a "producer" route, or the mesh implementation must support
+ and allow "consumer" routes for the referenced Service. ReferenceGrant is
+ not applicable for governing ParentRefs to Services - it is not possible to
+ create a "producer" route for a Service in a different namespace from the
+ Route.
+
+
+ There are two kinds of parent resources with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ This API may be extended in the future to support additional kinds of parent
+ resources.
+
+
+ ParentRefs must be _distinct_. This means either that:
+
+
+ * They select different objects. If this is the case, then parentRef
+ entries are distinct. In terms of fields, this means that the
+ multi-part key defined by `group`, `kind`, `namespace`, and `name` must
+ be unique across all parentRef entries in the Route.
+ * They do not select different objects, but for each optional field used,
+ each ParentRef that selects the same object must set the same set of
+ optional fields to different values. If one ParentRef sets a
+ combination of optional fields, all must set the same combination.
+
+
+ Some examples:
+
+
+ * If one ParentRef sets `sectionName`, all ParentRefs referencing the
+ same object must also set `sectionName`.
+ * If one ParentRef sets `port`, all ParentRefs referencing the same
+ object must also set `port`.
+ * If one ParentRef sets `sectionName` and `port`, all ParentRefs
+ referencing the same object must also set `sectionName` and `port`.
+
+
+ It is possible to separately reference multiple distinct objects that may
+ be collapsed by an implementation. For example, some implementations may
+ choose to merge compatible Gateway Listeners together. If that is the
+ case, the list of routes attached to those resources should also be
+ merged.
+
+
+ Note that for ParentRefs that cross namespace boundaries, there are specific
+ rules. Cross-namespace references are only valid if they are explicitly
+ allowed by something in the namespace they are referring to. For example,
+ Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+ generic way to enable other kinds of cross-namespace reference.
+
+
+
+
+
+
+
+
+ items:
+ description: |-
+ ParentReference identifies an API object (usually a Gateway) that can be considered
+ a parent of this resource (usually a route). There are two kinds of parent resources
+ with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ This API may be extended in the future to support additional kinds of parent
+ resources.
+
+
+ The API object must be valid in the cluster; the Group and Kind must
+ be registered in the cluster for this reference to be valid.
+ properties:
+ group:
+ default: gateway.networking.k8s.io
+ description: |-
+ Group is the group of the referent.
+ When unspecified, "gateway.networking.k8s.io" is inferred.
+ To set the core API group (such as for a "Service" kind referent),
+ Group must be explicitly set to "" (empty string).
+
+
+ Support: Core
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Gateway
+ description: |-
+ Kind is kind of the referent.
+
+
+ There are two kinds of parent resources with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ Support for other resources is Implementation-Specific.
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: |-
+ Name is the name of the referent.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the referent. When unspecified, this refers
+ to the local namespace of the Route.
+
+
+ Note that there are specific rules for ParentRefs which cross namespace
+ boundaries. Cross-namespace references are only valid if they are explicitly
+ allowed by something in the namespace they are referring to. For example:
+ Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+ generic way to enable any other kind of cross-namespace reference.
+
+
+
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port is the network port this Route targets. It can be interpreted
+ differently based on the type of parent resource.
+
+
+ When the parent resource is a Gateway, this targets all listeners
+ listening on the specified port that also support this kind of Route(and
+ select this Route). It's not recommended to set `Port` unless the
+ networking behaviors specified in a Route must apply to a specific port
+ as opposed to a listener(s) whose port(s) may be changed. When both Port
+ and SectionName are specified, the name and port of the selected listener
+ must match both specified values.
+
+
+
+
+
+ Implementations MAY choose to support other parent resources.
+ Implementations supporting other types of parent resources MUST clearly
+ document how/if Port is interpreted.
+
+
+ For the purpose of status, an attachment is considered successful as
+ long as the parent resource accepts it partially. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
+ from the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route,
+ the Route MUST be considered detached from the Gateway.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ sectionName:
+ description: |-
+ SectionName is the name of a section within the target resource. In the
+ following resources, SectionName is interpreted as the following:
+
+
+ * Gateway: Listener name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+ * Service: Port name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+
+
+ Implementations MAY choose to support attaching Routes to other resources.
+ If that is the case, they MUST clearly document how SectionName is
+ interpreted.
+
+
+ When unspecified (empty string), this will reference the entire resource.
+ For the purpose of status, an attachment is considered successful if at
+ least one section in the parent resource accepts it. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
+ the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route, the
+ Route MUST be considered detached from the Gateway.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - name
+ type: object
+ maxItems: 32
+ type: array
+ x-kubernetes-validations:
+ - message: sectionName must be specified when parentRefs includes
+ 2 or more references to the same parent
+ rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind
+ == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__)
+ || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__
+ == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) &&
+ p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName)
+ || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName
+ == '''')) : true))'
+ - message: sectionName must be unique when parentRefs includes 2 or
+ more references to the same parent
+ rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind
+ == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__)
+ || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__
+ == '')) || (has(p1.__namespace__) && has(p2.__namespace__) &&
+ p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName)
+ || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName
+ == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName
+ == p2.sectionName))))
+ rules:
+ default:
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /
+ description: Rules are a list of HTTP matchers, filters and actions.
+ items:
+ description: |-
+ HTTPRouteRule defines semantics for matching an HTTP request based on
+ conditions (matches), processing it (filters), and forwarding the request to
+ an API object (backendRefs).
+ properties:
+ backendRefs:
+ description: |-
+ BackendRefs defines the backend(s) where matching requests should be
+ sent.
+
+
+ Failure behavior here depends on how many BackendRefs are specified and
+ how many are invalid.
+
+
+ If *all* entries in BackendRefs are invalid, and there are also no filters
+ specified in this route rule, *all* traffic which matches this rule MUST
+ receive a 500 status code.
+
+
+ See the HTTPBackendRef definition for the rules about what makes a single
+ HTTPBackendRef invalid.
+
+
+ When a HTTPBackendRef is invalid, 500 status codes MUST be returned for
+ requests that would have otherwise been routed to an invalid backend. If
+ multiple backends are specified, and some are invalid, the proportion of
+ requests that would otherwise have been routed to an invalid backend
+ MUST receive a 500 status code.
+
+
+ For example, if two backends are specified with equal weights, and one is
+ invalid, 50 percent of traffic must receive a 500. Implementations may
+ choose how that 50 percent is determined.
+
+
+ Support: Core for Kubernetes Service
+
+
+ Support: Extended for Kubernetes ServiceImport
+
+
+ Support: Implementation-specific for any other resource
+
+
+ Support for weight: Core
+ items:
+ description: |-
+ HTTPBackendRef defines how a HTTPRoute forwards a HTTP request.
+
+
+ Note that when a namespace different than the local namespace is specified, a
+ ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+
+
+
+ When the BackendRef points to a Kubernetes Service, implementations SHOULD
+ honor the appProtocol field if it is set for the target Service Port.
+
+
+ Implementations supporting appProtocol SHOULD recognize the Kubernetes
+ Standard Application Protocols defined in KEP-3726.
+
+
+ If a Service appProtocol isn't specified, an implementation MAY infer the
+ backend protocol through its own means. Implementations MAY infer the
+ protocol from the Route type referring to the backend Service.
+
+
+ If a Route is not able to send traffic to the backend using the specified
+ protocol then the backend is considered invalid. Implementations MUST set the
+ "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason.
+
+
+
+ properties:
+ filters:
+ description: |-
+ Filters defined at this level should be executed if and only if the
+ request is being forwarded to the backend defined here.
+
+
+ Support: Implementation-specific (For broader support of filters, use the
+ Filters field in HTTPRouteRule.)
+ items:
+ description: |-
+ 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.
+ properties:
+ extensionRef:
+ description: |-
+ ExtensionRef is an optional, implementation-specific extension to the
+ "filter" behavior. For example, resource "myroutefilter" in group
+ "networking.example.net"). ExtensionRef MUST NOT be used for core and
+ extended filters.
+
+
+ This filter can be used multiple times within the same rule.
+
+
+ Support: Implementation-specific
+ properties:
+ group:
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ description: Kind is kind of the referent. For
+ example "HTTPRoute" or "Service".
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - group
+ - kind
+ - name
+ type: object
+ requestHeaderModifier:
+ description: |-
+ RequestHeaderModifier defines a schema for a filter that modifies request
+ headers.
+
+
+ Support: Core
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ requestMirror:
+ description: |-
+ RequestMirror defines a schema for a filter that mirrors requests.
+ Requests are sent to the specified destination, but responses from
+ that destination are ignored.
+
+
+ This filter can be used multiple times within the same rule. Note that
+ not all implementations will be able to support mirroring to multiple
+ backends.
+
+
+ Support: Extended
+ properties:
+ backendRef:
+ description: |-
+ BackendRef references a resource where mirrored requests are sent.
+
+
+ Mirrored requests must be sent only to a single destination endpoint
+ within this BackendRef, irrespective of how many endpoints are present
+ within this BackendRef.
+
+
+ If the referent cannot be found, this BackendRef is invalid and must be
+ dropped from the Gateway. The controller must ensure the "ResolvedRefs"
+ condition on the Route status is set to `status: False` and not configure
+ this backend in the underlying implementation.
+
+
+ If there is a cross-namespace reference to an *existing* object
+ that is not allowed by a ReferenceGrant, the controller must ensure the
+ "ResolvedRefs" condition on the Route is set to `status: False`,
+ with the "RefNotPermitted" reason and not configure this backend in the
+ underlying implementation.
+
+
+ In either error case, the Message of the `ResolvedRefs` Condition
+ should be used to provide more detail about the problem.
+
+
+ Support: Extended for Kubernetes Service
+
+
+ Support: Implementation-specific for any other resource
+ properties:
+ group:
+ default: ""
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Service
+ description: |-
+ Kind is the Kubernetes resource kind of the referent. For example
+ "Service".
+
+
+ Defaults to "Service" when not specified.
+
+
+ ExternalName services can refer to CNAME DNS records that may live
+ outside of the cluster and as such are difficult to reason about in
+ terms of conformance. They also may not be safe to forward to (see
+ CVE-2021-25740 for more information). Implementations SHOULD NOT
+ support ExternalName Services.
+
+
+ Support: Core (Services with a type other than ExternalName)
+
+
+ Support: Implementation-specific (Services with type ExternalName)
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the backend. When unspecified, the local
+ namespace is inferred.
+
+
+ Note that when a namespace different than the local namespace is specified,
+ a ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port specifies the destination port number to use for this resource.
+ Port is required when the referent is a Kubernetes Service. In this
+ case, the port number is the service port number, not the target port.
+ For other resources, destination port might be derived from the referent
+ resource or this field.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must have port for Service reference
+ rule: '(size(self.group) == 0 && self.kind
+ == ''Service'') ? has(self.port) : true'
+ required:
+ - backendRef
+ type: object
+ requestRedirect:
+ description: |-
+ RequestRedirect defines a schema for a filter that responds to the
+ request with an HTTP redirection.
+
+
+ Support: Core
+ properties:
+ hostname:
+ description: |-
+ Hostname is the hostname to be used in the value of the `Location`
+ header in the response.
+ When empty, the hostname in the `Host` header of the request is used.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines parameters used to modify the path of the incoming request.
+ The modified path is then used to construct the `Location` header. When
+ empty, the request path is used as-is.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified
+ when type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ?
+ has(self.replaceFullPath) : true'
+ - message: type must be 'ReplaceFullPath' when
+ replaceFullPath is set
+ rule: 'has(self.replaceFullPath) ? self.type
+ == ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified
+ when type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch''
+ ? has(self.replacePrefixMatch) : true'
+ - message: type must be 'ReplacePrefixMatch'
+ when replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ port:
+ description: |-
+ Port is the port to be used in the value of the `Location`
+ header in the response.
+
+
+ If no port is specified, the redirect port MUST be derived using the
+ following rules:
+
+
+ * If redirect scheme is not-empty, the redirect port MUST be the well-known
+ port associated with the redirect scheme. Specifically "http" to port 80
+ and "https" to port 443. If the redirect scheme does not have a
+ well-known port, the listener port of the Gateway SHOULD be used.
+ * If redirect scheme is empty, the redirect port MUST be the Gateway
+ Listener port.
+
+
+ Implementations SHOULD NOT add the port number in the 'Location'
+ header in the following cases:
+
+
+ * A Location header that will use HTTP (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 80.
+ * A Location header that will use HTTPS (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 443.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ scheme:
+ description: |-
+ Scheme is the scheme to be used in the value of the `Location` header in
+ the response. When empty, the scheme of the request is used.
+
+
+ Scheme redirects can affect the port of the redirect, for more information,
+ refer to the documentation for the port field of this filter.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Extended
+ enum:
+ - http
+ - https
+ type: string
+ statusCode:
+ default: 302
+ description: |-
+ StatusCode is the HTTP status code to be used in response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Core
+ enum:
+ - 301
+ - 302
+ type: integer
+ type: object
+ responseHeaderModifier:
+ description: |-
+ ResponseHeaderModifier defines a schema for a filter that modifies response
+ headers.
+
+
+ Support: Extended
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP
+ Header name and value as defined by RFC
+ 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP
+ Header to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ type:
+ description: |-
+ Type identifies the type of filter to apply. As with other API fields,
+ types are classified into three conformance levels:
+
+
+ - Core: Filter types and their corresponding configuration defined by
+ "Support: Core" in this package, e.g. "RequestHeaderModifier". All
+ implementations must support core filters.
+
+
+ - Extended: Filter types and their corresponding configuration defined by
+ "Support: Extended" in this package, e.g. "RequestMirror". Implementers
+ are encouraged to support extended filters.
+
+
+ - Implementation-specific: Filters that are defined and supported by
+ specific vendors.
+ In the future, filters showing convergence in behavior across multiple
+ implementations will be considered for inclusion in extended or core
+ conformance levels. Filter-specific configuration for such filters
+ is specified using the ExtensionRef field. `Type` should be set to
+ "ExtensionRef" for custom filters.
+
+
+ Implementers are encouraged to define custom implementation types to
+ extend the core API with implementation-specific behavior.
+
+
+ If a reference to a custom filter type cannot be resolved, the filter
+ MUST NOT be skipped. Instead, requests that would have been processed by
+ that filter MUST receive a HTTP error response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - RequestHeaderModifier
+ - ResponseHeaderModifier
+ - RequestMirror
+ - RequestRedirect
+ - URLRewrite
+ - ExtensionRef
+ type: string
+ urlRewrite:
+ description: |-
+ URLRewrite defines a schema for a filter that modifies a request during forwarding.
+
+
+ Support: Extended
+ properties:
+ hostname:
+ description: |-
+ Hostname is the value to be used to replace the Host header value during
+ forwarding.
+
+
+ Support: Extended
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines a path rewrite.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified
+ when type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ?
+ has(self.replaceFullPath) : true'
+ - message: type must be 'ReplaceFullPath' when
+ replaceFullPath is set
+ rule: 'has(self.replaceFullPath) ? self.type
+ == ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified
+ when type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch''
+ ? has(self.replacePrefixMatch) : true'
+ - message: type must be 'ReplacePrefixMatch'
+ when replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ type: object
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: filter.requestHeaderModifier must be nil
+ if the filter.type is not RequestHeaderModifier
+ rule: '!(has(self.requestHeaderModifier) && self.type
+ != ''RequestHeaderModifier'')'
+ - message: filter.requestHeaderModifier must be specified
+ for RequestHeaderModifier filter.type
+ rule: '!(!has(self.requestHeaderModifier) && self.type
+ == ''RequestHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be nil
+ if the filter.type is not ResponseHeaderModifier
+ rule: '!(has(self.responseHeaderModifier) && self.type
+ != ''ResponseHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be specified
+ for ResponseHeaderModifier filter.type
+ rule: '!(!has(self.responseHeaderModifier) && self.type
+ == ''ResponseHeaderModifier'')'
+ - message: filter.requestMirror must be nil if the filter.type
+ is not RequestMirror
+ rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')'
+ - message: filter.requestMirror must be specified for
+ RequestMirror filter.type
+ rule: '!(!has(self.requestMirror) && self.type ==
+ ''RequestMirror'')'
+ - message: filter.requestRedirect must be nil if the
+ filter.type is not RequestRedirect
+ rule: '!(has(self.requestRedirect) && self.type !=
+ ''RequestRedirect'')'
+ - message: filter.requestRedirect must be specified
+ for RequestRedirect filter.type
+ rule: '!(!has(self.requestRedirect) && self.type ==
+ ''RequestRedirect'')'
+ - message: filter.urlRewrite must be nil if the filter.type
+ is not URLRewrite
+ rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')'
+ - message: filter.urlRewrite must be specified for URLRewrite
+ filter.type
+ rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')'
+ - message: filter.extensionRef must be nil if the filter.type
+ is not ExtensionRef
+ rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')'
+ - message: filter.extensionRef must be specified for
+ ExtensionRef filter.type
+ rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')'
+ maxItems: 16
+ type: array
+ x-kubernetes-validations:
+ - message: May specify either httpRouteFilterRequestRedirect
+ or httpRouteFilterRequestRewrite, but not both
+ rule: '!(self.exists(f, f.type == ''RequestRedirect'')
+ && self.exists(f, f.type == ''URLRewrite''))'
+ - message: May specify either httpRouteFilterRequestRedirect
+ or httpRouteFilterRequestRewrite, but not both
+ rule: '!(self.exists(f, f.type == ''RequestRedirect'')
+ && self.exists(f, f.type == ''URLRewrite''))'
+ - message: RequestHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestHeaderModifier').size()
+ <= 1
+ - message: ResponseHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'ResponseHeaderModifier').size()
+ <= 1
+ - message: RequestRedirect filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestRedirect').size()
+ <= 1
+ - message: URLRewrite filter cannot be repeated
+ rule: self.filter(f, f.type == 'URLRewrite').size()
+ <= 1
+ group:
+ default: ""
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Service
+ description: |-
+ Kind is the Kubernetes resource kind of the referent. For example
+ "Service".
+
+
+ Defaults to "Service" when not specified.
+
+
+ ExternalName services can refer to CNAME DNS records that may live
+ outside of the cluster and as such are difficult to reason about in
+ terms of conformance. They also may not be safe to forward to (see
+ CVE-2021-25740 for more information). Implementations SHOULD NOT
+ support ExternalName Services.
+
+
+ Support: Core (Services with a type other than ExternalName)
+
+
+ Support: Implementation-specific (Services with type ExternalName)
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the backend. When unspecified, the local
+ namespace is inferred.
+
+
+ Note that when a namespace different than the local namespace is specified,
+ a ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port specifies the destination port number to use for this resource.
+ Port is required when the referent is a Kubernetes Service. In this
+ case, the port number is the service port number, not the target port.
+ For other resources, destination port might be derived from the referent
+ resource or this field.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ weight:
+ default: 1
+ description: |-
+ Weight specifies the proportion of requests forwarded to the referenced
+ backend. This is computed as weight/(sum of all weights in this
+ BackendRefs list). For non-zero values, there may be some epsilon from
+ the exact proportion defined here depending on the precision an
+ implementation supports. Weight is not a percentage and the sum of
+ weights does not need to equal 100.
+
+
+ If only one backend is specified and it has a weight greater than 0, 100%
+ of the traffic is forwarded to that backend. If weight is set to 0, no
+ traffic should be forwarded for this entry. If unspecified, weight
+ defaults to 1.
+
+
+ Support for this field varies based on the context where used.
+ format: int32
+ maximum: 1000000
+ minimum: 0
+ type: integer
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must have port for Service reference
+ rule: '(size(self.group) == 0 && self.kind == ''Service'')
+ ? has(self.port) : true'
+ maxItems: 16
+ type: array
+ filters:
+ description: |-
+ Filters define the filters that are applied to requests that match
+ this rule.
+
+
+ Wherever possible, implementations SHOULD implement filters in the order
+ they are specified.
+
+
+ Implementations MAY choose to implement this ordering strictly, rejecting
+ any combination or order of filters that can not be supported. If implementations
+ choose a strict interpretation of filter ordering, they MUST clearly document
+ that behavior.
+
+
+ To reject an invalid combination or order of filters, implementations SHOULD
+ consider the Route Rules with this configuration invalid. If all Route Rules
+ in a Route are invalid, the entire Route would be considered invalid. If only
+ a portion of Route Rules are invalid, implementations MUST set the
+ "PartiallyInvalid" condition for the Route.
+
+
+ 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 the same filter multiple times is not supported unless explicitly
+ indicated in the filter.
+
+
+ All filters are expected to be compatible with each other except for the
+ URLRewrite and RequestRedirect filters, which may not be combined. If an
+ implementation can not support other combinations of filters, they must clearly
+ document that limitation. In cases where incompatible or unsupported
+ filters are specified and cause the `Accepted` condition to be set to status
+ `False`, implementations may use the `IncompatibleFilters` reason to specify
+ this configuration error.
+
+
+ Support: Core
+ items:
+ description: |-
+ 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.
+ properties:
+ extensionRef:
+ description: |-
+ ExtensionRef is an optional, implementation-specific extension to the
+ "filter" behavior. For example, resource "myroutefilter" in group
+ "networking.example.net"). ExtensionRef MUST NOT be used for core and
+ extended filters.
+
+
+ This filter can be used multiple times within the same rule.
+
+
+ Support: Implementation-specific
+ properties:
+ group:
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ description: Kind is kind of the referent. For example
+ "HTTPRoute" or "Service".
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - group
+ - kind
+ - name
+ type: object
+ requestHeaderModifier:
+ description: |-
+ RequestHeaderModifier defines a schema for a filter that modifies request
+ headers.
+
+
+ Support: Core
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ requestMirror:
+ description: |-
+ RequestMirror defines a schema for a filter that mirrors requests.
+ Requests are sent to the specified destination, but responses from
+ that destination are ignored.
+
+
+ This filter can be used multiple times within the same rule. Note that
+ not all implementations will be able to support mirroring to multiple
+ backends.
+
+
+ Support: Extended
+ properties:
+ backendRef:
+ description: |-
+ BackendRef references a resource where mirrored requests are sent.
+
+
+ Mirrored requests must be sent only to a single destination endpoint
+ within this BackendRef, irrespective of how many endpoints are present
+ within this BackendRef.
+
+
+ If the referent cannot be found, this BackendRef is invalid and must be
+ dropped from the Gateway. The controller must ensure the "ResolvedRefs"
+ condition on the Route status is set to `status: False` and not configure
+ this backend in the underlying implementation.
+
+
+ If there is a cross-namespace reference to an *existing* object
+ that is not allowed by a ReferenceGrant, the controller must ensure the
+ "ResolvedRefs" condition on the Route is set to `status: False`,
+ with the "RefNotPermitted" reason and not configure this backend in the
+ underlying implementation.
+
+
+ In either error case, the Message of the `ResolvedRefs` Condition
+ should be used to provide more detail about the problem.
+
+
+ Support: Extended for Kubernetes Service
+
+
+ Support: Implementation-specific for any other resource
+ properties:
+ group:
+ default: ""
+ description: |-
+ Group is the group of the referent. For example, "gateway.networking.k8s.io".
+ When unspecified or empty string, core API group is inferred.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Service
+ description: |-
+ Kind is the Kubernetes resource kind of the referent. For example
+ "Service".
+
+
+ Defaults to "Service" when not specified.
+
+
+ ExternalName services can refer to CNAME DNS records that may live
+ outside of the cluster and as such are difficult to reason about in
+ terms of conformance. They also may not be safe to forward to (see
+ CVE-2021-25740 for more information). Implementations SHOULD NOT
+ support ExternalName Services.
+
+
+ Support: Core (Services with a type other than ExternalName)
+
+
+ Support: Implementation-specific (Services with type ExternalName)
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the backend. When unspecified, the local
+ namespace is inferred.
+
+
+ Note that when a namespace different than the local namespace is specified,
+ a ReferenceGrant object is required in the referent namespace to allow that
+ namespace's owner to accept the reference. See the ReferenceGrant
+ documentation for details.
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port specifies the destination port number to use for this resource.
+ Port is required when the referent is a Kubernetes Service. In this
+ case, the port number is the service port number, not the target port.
+ For other resources, destination port might be derived from the referent
+ resource or this field.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: Must have port for Service reference
+ rule: '(size(self.group) == 0 && self.kind == ''Service'')
+ ? has(self.port) : true'
+ required:
+ - backendRef
+ type: object
+ requestRedirect:
+ description: |-
+ RequestRedirect defines a schema for a filter that responds to the
+ request with an HTTP redirection.
+
+
+ Support: Core
+ properties:
+ hostname:
+ description: |-
+ Hostname is the hostname to be used in the value of the `Location`
+ header in the response.
+ When empty, the hostname in the `Host` header of the request is used.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines parameters used to modify the path of the incoming request.
+ The modified path is then used to construct the `Location` header. When
+ empty, the request path is used as-is.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified when
+ type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath)
+ : true'
+ - message: type must be 'ReplaceFullPath' when replaceFullPath
+ is set
+ rule: 'has(self.replaceFullPath) ? self.type ==
+ ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified when
+ type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch)
+ : true'
+ - message: type must be 'ReplacePrefixMatch' when
+ replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ port:
+ description: |-
+ Port is the port to be used in the value of the `Location`
+ header in the response.
+
+
+ If no port is specified, the redirect port MUST be derived using the
+ following rules:
+
+
+ * If redirect scheme is not-empty, the redirect port MUST be the well-known
+ port associated with the redirect scheme. Specifically "http" to port 80
+ and "https" to port 443. If the redirect scheme does not have a
+ well-known port, the listener port of the Gateway SHOULD be used.
+ * If redirect scheme is empty, the redirect port MUST be the Gateway
+ Listener port.
+
+
+ Implementations SHOULD NOT add the port number in the 'Location'
+ header in the following cases:
+
+
+ * A Location header that will use HTTP (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 80.
+ * A Location header that will use HTTPS (whether that is determined via
+ the Listener protocol or the Scheme field) _and_ use port 443.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ scheme:
+ description: |-
+ Scheme is the scheme to be used in the value of the `Location` header in
+ the response. When empty, the scheme of the request is used.
+
+
+ Scheme redirects can affect the port of the redirect, for more information,
+ refer to the documentation for the port field of this filter.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Extended
+ enum:
+ - http
+ - https
+ type: string
+ statusCode:
+ default: 302
+ description: |-
+ StatusCode is the HTTP status code to be used in response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+
+
+ Support: Core
+ enum:
+ - 301
+ - 302
+ type: integer
+ type: object
+ responseHeaderModifier:
+ description: |-
+ ResponseHeaderModifier defines a schema for a filter that modifies response
+ headers.
+
+
+ Support: Extended
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP Header
+ name and value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header
+ to be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ type:
+ description: |-
+ Type identifies the type of filter to apply. As with other API fields,
+ types are classified into three conformance levels:
+
+
+ - Core: Filter types and their corresponding configuration defined by
+ "Support: Core" in this package, e.g. "RequestHeaderModifier". All
+ implementations must support core filters.
+
+
+ - Extended: Filter types and their corresponding configuration defined by
+ "Support: Extended" in this package, e.g. "RequestMirror". Implementers
+ are encouraged to support extended filters.
+
+
+ - Implementation-specific: Filters that are defined and supported by
+ specific vendors.
+ In the future, filters showing convergence in behavior across multiple
+ implementations will be considered for inclusion in extended or core
+ conformance levels. Filter-specific configuration for such filters
+ is specified using the ExtensionRef field. `Type` should be set to
+ "ExtensionRef" for custom filters.
+
+
+ Implementers are encouraged to define custom implementation types to
+ extend the core API with implementation-specific behavior.
+
+
+ If a reference to a custom filter type cannot be resolved, the filter
+ MUST NOT be skipped. Instead, requests that would have been processed by
+ that filter MUST receive a HTTP error response.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - RequestHeaderModifier
+ - ResponseHeaderModifier
+ - RequestMirror
+ - RequestRedirect
+ - URLRewrite
+ - ExtensionRef
+ type: string
+ urlRewrite:
+ description: |-
+ URLRewrite defines a schema for a filter that modifies a request during forwarding.
+
+
+ Support: Extended
+ properties:
+ hostname:
+ description: |-
+ Hostname is the value to be used to replace the Host header value during
+ forwarding.
+
+
+ Support: Extended
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ path:
+ description: |-
+ Path defines a path rewrite.
+
+
+ Support: Extended
+ properties:
+ replaceFullPath:
+ description: |-
+ ReplaceFullPath specifies the value with which to replace the full path
+ of a request during a rewrite or redirect.
+ maxLength: 1024
+ type: string
+ replacePrefixMatch:
+ description: |-
+ ReplacePrefixMatch specifies the value with which to replace the prefix
+ match of a request during a rewrite or redirect. For example, a request
+ to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch
+ of "/xyz" would be modified to "/xyz/bar".
+
+
+ Note that this matches the behavior of the PathPrefix match type. This
+ matches full path elements. A path element refers to the list of labels
+ in the path split by the `/` separator. When specified, a trailing `/` is
+ ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all
+ match the prefix `/abc`, but the path `/abcd` would not.
+
+
+ ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch.
+ Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in
+ the implementation setting the Accepted Condition for the Route to `status: False`.
+
+
+ Request Path | Prefix Match | Replace Prefix | Modified Path
+ -------------|--------------|----------------|----------
+ /foo/bar | /foo | /xyz | /xyz/bar
+ /foo/bar | /foo | /xyz/ | /xyz/bar
+ /foo/bar | /foo/ | /xyz | /xyz/bar
+ /foo/bar | /foo/ | /xyz/ | /xyz/bar
+ /foo | /foo | /xyz | /xyz
+ /foo/ | /foo | /xyz | /xyz/
+ /foo/bar | /foo | | /bar
+ /foo/ | /foo | | /
+ /foo | /foo | | /
+ /foo/ | /foo | / | /
+ /foo | /foo | / | /
+ maxLength: 1024
+ type: string
+ type:
+ description: |-
+ Type defines the type of path modifier. Additional types may be
+ added in a future release of the API.
+
+
+ Note that values may be added to this enum, implementations
+ must ensure that unknown values will not cause a crash.
+
+
+ Unknown values here must result in the implementation setting the
+ Accepted Condition for the Route to `status: False`, with a
+ Reason of `UnsupportedValue`.
+ enum:
+ - ReplaceFullPath
+ - ReplacePrefixMatch
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: replaceFullPath must be specified when
+ type is set to 'ReplaceFullPath'
+ rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath)
+ : true'
+ - message: type must be 'ReplaceFullPath' when replaceFullPath
+ is set
+ rule: 'has(self.replaceFullPath) ? self.type ==
+ ''ReplaceFullPath'' : true'
+ - message: replacePrefixMatch must be specified when
+ type is set to 'ReplacePrefixMatch'
+ rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch)
+ : true'
+ - message: type must be 'ReplacePrefixMatch' when
+ replacePrefixMatch is set
+ rule: 'has(self.replacePrefixMatch) ? self.type
+ == ''ReplacePrefixMatch'' : true'
+ type: object
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: filter.requestHeaderModifier must be nil if the
+ filter.type is not RequestHeaderModifier
+ rule: '!(has(self.requestHeaderModifier) && self.type !=
+ ''RequestHeaderModifier'')'
+ - message: filter.requestHeaderModifier must be specified
+ for RequestHeaderModifier filter.type
+ rule: '!(!has(self.requestHeaderModifier) && self.type ==
+ ''RequestHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be nil if the
+ filter.type is not ResponseHeaderModifier
+ rule: '!(has(self.responseHeaderModifier) && self.type !=
+ ''ResponseHeaderModifier'')'
+ - message: filter.responseHeaderModifier must be specified
+ for ResponseHeaderModifier filter.type
+ rule: '!(!has(self.responseHeaderModifier) && self.type
+ == ''ResponseHeaderModifier'')'
+ - message: filter.requestMirror must be nil if the filter.type
+ is not RequestMirror
+ rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')'
+ - message: filter.requestMirror must be specified for RequestMirror
+ filter.type
+ rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')'
+ - message: filter.requestRedirect must be nil if the filter.type
+ is not RequestRedirect
+ rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')'
+ - message: filter.requestRedirect must be specified for RequestRedirect
+ filter.type
+ rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')'
+ - message: filter.urlRewrite must be nil if the filter.type
+ is not URLRewrite
+ rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')'
+ - message: filter.urlRewrite must be specified for URLRewrite
+ filter.type
+ rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')'
+ - message: filter.extensionRef must be nil if the filter.type
+ is not ExtensionRef
+ rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')'
+ - message: filter.extensionRef must be specified for ExtensionRef
+ filter.type
+ rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')'
+ maxItems: 16
+ type: array
+ x-kubernetes-validations:
+ - message: May specify either httpRouteFilterRequestRedirect
+ or httpRouteFilterRequestRewrite, but not both
+ rule: '!(self.exists(f, f.type == ''RequestRedirect'') &&
+ self.exists(f, f.type == ''URLRewrite''))'
+ - message: RequestHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestHeaderModifier').size()
+ <= 1
+ - message: ResponseHeaderModifier filter cannot be repeated
+ rule: self.filter(f, f.type == 'ResponseHeaderModifier').size()
+ <= 1
+ - message: RequestRedirect filter cannot be repeated
+ rule: self.filter(f, f.type == 'RequestRedirect').size() <=
+ 1
+ - message: URLRewrite filter cannot be repeated
+ rule: self.filter(f, f.type == 'URLRewrite').size() <= 1
+ matches:
+ default:
+ - path:
+ type: PathPrefix
+ value: /
+ description: |-
+ 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:
+
+
+ ```
+ 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 matches based on the following criteria, continuing on
+ ties. Across all rules specified on applicable Routes, precedence must be
+ given to the match having:
+
+
+ * "Exact" path match.
+ * "Prefix" path match with largest number of characters.
+ * Method match.
+ * Largest number of header matches.
+ * Largest number of query param matches.
+
+
+ Note: The precedence of RegularExpression path matches are implementation-specific.
+
+
+ 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 an HTTPRoute, matching precedence MUST be granted
+ to the FIRST matching rule (in list order) with a match 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.
+ items:
+ description: "HTTPRouteMatch defines the predicate used to
+ match requests to a given\naction. Multiple match types
+ are ANDed together, i.e. the match will\nevaluate to true
+ only if all conditions are satisfied.\n\n\nFor example,
+ the match below will match a HTTP request only if its path\nstarts
+ with `/foo` AND it contains the `version: v1` header:\n\n\n```\nmatch:\n\n\n\tpath:\n\t
+ \ value: \"/foo\"\n\theaders:\n\t- name: \"version\"\n\t
+ \ value \"v1\"\n\n\n```"
+ properties:
+ headers:
+ description: |-
+ Headers specifies HTTP request header matchers. Multiple match values are
+ ANDed together, meaning, a request must match all the specified headers
+ to select the route.
+ items:
+ description: |-
+ HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request
+ headers.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, only the first
+ entry with an equivalent name MUST be considered for a match. Subsequent
+ entries with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+
+
+ When a header is repeated in an HTTP request, it is
+ implementation-specific behavior as to how this is represented.
+ Generally, proxies should follow the guidance from the RFC:
+ https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 regarding
+ processing a repeated header, with special handling for "Set-Cookie".
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ type:
+ default: Exact
+ description: |-
+ Type specifies how to match against the value of the header.
+
+
+ Support: Core (Exact)
+
+
+ Support: Implementation-specific (RegularExpression)
+
+
+ Since RegularExpression HeaderMatchType has implementation-specific
+ conformance, implementations can support POSIX, PCRE or any other dialects
+ of regular expressions. Please read the implementation's documentation to
+ determine the supported dialect.
+ enum:
+ - Exact
+ - RegularExpression
+ type: string
+ value:
+ description: Value is the value of HTTP Header to
+ be matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ method:
+ description: |-
+ Method specifies HTTP method matcher.
+ When specified, this route will be matched only if the request has the
+ specified method.
+
+
+ Support: Extended
+ enum:
+ - GET
+ - HEAD
+ - POST
+ - PUT
+ - DELETE
+ - CONNECT
+ - OPTIONS
+ - TRACE
+ - PATCH
+ type: string
+ path:
+ default:
+ type: PathPrefix
+ value: /
+ description: |-
+ Path specifies a HTTP request path matcher. If this field is not
+ specified, a default prefix match on the "/" path is provided.
+ properties:
+ type:
+ default: PathPrefix
+ description: |-
+ Type specifies how to match against the path Value.
+
+
+ Support: Core (Exact, PathPrefix)
+
+
+ Support: Implementation-specific (RegularExpression)
+ enum:
+ - Exact
+ - PathPrefix
+ - RegularExpression
+ type: string
+ value:
+ default: /
+ description: Value of the HTTP path to match against.
+ maxLength: 1024
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: value must be an absolute path and start with
+ '/' when type one of ['Exact', 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.startsWith(''/'')
+ : true'
+ - message: must not contain '//' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''//'')
+ : true'
+ - message: must not contain '/./' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/./'')
+ : true'
+ - message: must not contain '/../' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/../'')
+ : true'
+ - message: must not contain '%2f' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2f'')
+ : true'
+ - message: must not contain '%2F' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2F'')
+ : true'
+ - message: must not contain '#' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''#'')
+ : true'
+ - message: must not end with '/..' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/..'')
+ : true'
+ - message: must not end with '/.' when type one of ['Exact',
+ 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/.'')
+ : true'
+ - message: type must be one of ['Exact', 'PathPrefix',
+ 'RegularExpression']
+ rule: self.type in ['Exact','PathPrefix'] || self.type
+ == 'RegularExpression'
+ - message: must only contain valid characters (matching
+ ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$)
+ for types ['Exact', 'PathPrefix']
+ rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""")
+ : true'
+ queryParams:
+ description: |-
+ QueryParams specifies HTTP query parameter matchers. Multiple match
+ values are ANDed together, meaning, a request must match all the
+ specified query parameters to select the route.
+
+
+ Support: Extended
+ items:
+ description: |-
+ HTTPQueryParamMatch describes how to select a HTTP route by matching HTTP
+ query parameters.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP query param to be matched. This must be an
+ exact string match. (See
+ https://tools.ietf.org/html/rfc7230#section-2.7.3).
+
+
+ If multiple entries specify equivalent query param names, only the first
+ entry with an equivalent name MUST be considered for a match. Subsequent
+ entries with an equivalent query param name MUST be ignored.
+
+
+ If a query param is repeated in an HTTP request, the behavior is
+ purposely left undefined, since different data planes have different
+ capabilities. However, it is *recommended* that implementations should
+ match against the first value of the param if the data plane supports it,
+ as this behavior is expected in other load balancing contexts outside of
+ the Gateway API.
+
+
+ Users SHOULD NOT route traffic based on repeated query params to guard
+ themselves against potential differences in the implementations.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ type:
+ default: Exact
+ description: |-
+ Type specifies how to match against the value of the query parameter.
+
+
+ Support: Extended (Exact)
+
+
+ Support: Implementation-specific (RegularExpression)
+
+
+ Since RegularExpression QueryParamMatchType has Implementation-specific
+ conformance, implementations can support POSIX, PCRE or any other
+ dialects of regular expressions. Please read the implementation's
+ documentation to determine the supported dialect.
+ enum:
+ - Exact
+ - RegularExpression
+ type: string
+ value:
+ description: Value is the value of HTTP query param
+ to be matched.
+ maxLength: 1024
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
+ maxItems: 8
+ type: array
+ type: object
+ x-kubernetes-validations:
+ - message: RequestRedirect filter must not be used together with
+ backendRefs
+ rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ?
+ (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))):
+ true'
+ - message: When using RequestRedirect filter with path.replacePrefixMatch,
+ exactly one PathPrefix match must be specified
+ rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect)
+ && has(f.requestRedirect.path) && f.requestRedirect.path.type
+ == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch)))
+ ? ((size(self.matches) != 1 || !has(self.matches[0].path) ||
+ self.matches[0].path.type != ''PathPrefix'') ? false : true)
+ : true'
+ - message: When using URLRewrite filter with path.replacePrefixMatch,
+ exactly one PathPrefix match must be specified
+ rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite)
+ && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch''
+ && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches)
+ != 1 || !has(self.matches[0].path) || self.matches[0].path.type
+ != ''PathPrefix'') ? false : true) : true'
+ - message: Within backendRefs, when using RequestRedirect filter
+ with path.replacePrefixMatch, exactly one PathPrefix match must
+ be specified
+ rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b,
+ (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect)
+ && has(f.requestRedirect.path) && f.requestRedirect.path.type
+ == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch)))
+ )) ? ((size(self.matches) != 1 || !has(self.matches[0].path)
+ || self.matches[0].path.type != ''PathPrefix'') ? false : true)
+ : true'
+ - message: Within backendRefs, When using URLRewrite filter with
+ path.replacePrefixMatch, exactly one PathPrefix match must be
+ specified
+ rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b,
+ (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite)
+ && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch''
+ && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches)
+ != 1 || !has(self.matches[0].path) || self.matches[0].path.type
+ != ''PathPrefix'') ? false : true) : true'
+ maxItems: 16
+ type: array
+ type: object
+ status:
+ description: Status defines the current state of HTTPRoute.
+ properties:
+ parents:
+ description: |-
+ Parents is a list of parent resources (usually Gateways) that are
+ associated with the route, and the status of the route with respect to
+ each parent. When this route attaches to a parent, the controller that
+ manages the parent must add an entry to this list when the controller
+ first sees the route and should update the entry as appropriate when the
+ route or gateway is modified.
+
+
+ Note that parent references that cannot be resolved by an implementation
+ of this API will not be added to this list. Implementations of this API
+ can only populate Route status for the Gateways/parent resources they are
+ responsible for.
+
+
+ A maximum of 32 Gateways will be represented in this list. An empty list
+ means the route has not been attached to any Gateway.
+ items:
+ description: |-
+ RouteParentStatus describes the status of a route with respect to an
+ associated Parent.
+ properties:
+ conditions:
+ description: |-
+ Conditions describes the status of the route with respect to the Gateway.
+ Note that the route's availability is also subject to the Gateway's own
+ status conditions and listener status.
+
+
+ If the Route's ParentRef specifies an existing Gateway that supports
+ Routes of this kind AND that Gateway's controller has sufficient access,
+ then that Gateway's controller MUST set the "Accepted" condition on the
+ Route, to indicate whether the route has been accepted or rejected by the
+ Gateway, and why.
+
+
+ A Route MUST be considered "Accepted" if at least one of the Route's
+ rules is implemented by the Gateway.
+
+
+ There are a number of cases where the "Accepted" condition may not be set
+ due to lack of controller visibility, that includes when:
+
+
+ * The Route refers to a non-existent parent.
+ * The Route is of a type that the controller does not support.
+ * The Route is in a namespace the controller does not have access to.
+ items:
+ description: "Condition contains details for one aspect of
+ the current state of this API Resource.\n---\nThis struct
+ is intended for direct use as an array at the field path
+ .status.conditions. For example,\n\n\n\ttype FooStatus
+ struct{\n\t // Represents the observations of a foo's
+ current state.\n\t // Known .status.conditions.type are:
+ \"Available\", \"Progressing\", and \"Degraded\"\n\t //
+ +patchMergeKey=type\n\t // +patchStrategy=merge\n\t //
+ +listType=map\n\t // +listMapKey=type\n\t Conditions
+ []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\"
+ patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+ \ // other fields\n\t}"
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False,
+ Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: |-
+ type of condition in CamelCase or in foo.example.com/CamelCase.
+ ---
+ Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+ useful (see .node.status.conditions), the ability to deconflict is important.
+ The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ maxItems: 8
+ minItems: 1
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ controllerName:
+ description: |-
+ ControllerName is a domain/path string that indicates the name of the
+ controller that wrote this status. This corresponds with the
+ controllerName field on GatewayClass.
+
+
+ Example: "example.net/gateway-controller".
+
+
+ The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
+ valid Kubernetes names
+ (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+
+
+ Controllers MUST populate this field when writing status. Controllers should ensure that
+ entries to status populated with their ControllerName are cleaned up when they are no
+ longer necessary.
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+ type: string
+ parentRef:
+ description: |-
+ ParentRef corresponds with a ParentRef in the spec that this
+ RouteParentStatus struct describes the status of.
+ properties:
+ group:
+ default: gateway.networking.k8s.io
+ description: |-
+ Group is the group of the referent.
+ When unspecified, "gateway.networking.k8s.io" is inferred.
+ To set the core API group (such as for a "Service" kind referent),
+ Group must be explicitly set to "" (empty string).
+
+
+ Support: Core
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Gateway
+ description: |-
+ Kind is kind of the referent.
+
+
+ There are two kinds of parent resources with "Core" support:
+
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+
+ Support for other resources is Implementation-Specific.
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: |-
+ Name is the name of the referent.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the referent. When unspecified, this refers
+ to the local namespace of the Route.
+
+
+ Note that there are specific rules for ParentRefs which cross namespace
+ boundaries. Cross-namespace references are only valid if they are explicitly
+ allowed by something in the namespace they are referring to. For example:
+ Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+ generic way to enable any other kind of cross-namespace reference.
+
+
+
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port is the network port this Route targets. It can be interpreted
+ differently based on the type of parent resource.
+
+
+ When the parent resource is a Gateway, this targets all listeners
+ listening on the specified port that also support this kind of Route(and
+ select this Route). It's not recommended to set `Port` unless the
+ networking behaviors specified in a Route must apply to a specific port
+ as opposed to a listener(s) whose port(s) may be changed. When both Port
+ and SectionName are specified, the name and port of the selected listener
+ must match both specified values.
+
+
+
+
+
+ Implementations MAY choose to support other parent resources.
+ Implementations supporting other types of parent resources MUST clearly
+ document how/if Port is interpreted.
+
+
+ For the purpose of status, an attachment is considered successful as
+ long as the parent resource accepts it partially. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
+ from the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route,
+ the Route MUST be considered detached from the Gateway.
+
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ sectionName:
+ description: |-
+ SectionName is the name of a section within the target resource. In the
+ following resources, SectionName is interpreted as the following:
+
+
+ * Gateway: Listener name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+ * Service: Port name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+
+
+ Implementations MAY choose to support attaching Routes to other resources.
+ If that is the case, they MUST clearly document how SectionName is
+ interpreted.
+
+
+ When unspecified (empty string), this will reference the entire resource.
+ For the purpose of status, an attachment is considered successful if at
+ least one section in the parent resource accepts it. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
+ the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route, the
+ Route MUST be considered detached from the Gateway.
+
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - name
+ type: object
+ required:
+ - controllerName
+ - parentRef
+ type: object
+ maxItems: 32
+ type: array
+ required:
+ - parents
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: false
+ subresources:
+ status: {}
+status:
+ acceptedNames:
+ kind: ""
+ plural: ""
+ conditions: null
+ storedVersions: null
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 25b554a..c2b186e 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -98,6 +98,18 @@ rules:
- patch
- update
- watch
+- apiGroups:
+ - gateway.networking.k8s.io
+ resources:
+ - httproutes
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
- apiGroups:
- monitoring.coreos.com
resources:
diff --git a/docs/agentrax.md b/docs/agentrax.md
index f1a1e18..e70f062 100644
--- a/docs/agentrax.md
+++ b/docs/agentrax.md
@@ -35,11 +35,21 @@ The canonical list of top-level end-to-end test scenarios. Do not add new top-le
| Phase | Focus | Status | Milestone |
| ----- | ------------------- | :---------: | ---------------------------------------------------------------------------------------- |
| 0 | Scaffolding | **Done** | `make install` applies CRDs; `make run` starts both controllers |
-| 1 | Core reconciliation | **Up Next** | `AgentDeployment` → Deployment/Service/ServiceMonitor; status/conditions; finalizer stub |
-| 2 | Multi-tenancy | Pending | `TenantQuota`, validating + mutating webhook, quota enforcement |
-| 3 | Autoscaling | Pending | Prometheus Adapter integration, managed HPA |
-| 4 | Canary rollout | Pending | Rollout state machine, Gateway API traffic shifting, PromQL threshold evaluation |
+| 1 | Core reconciliation | **Done** | `AgentDeployment` → Deployment/Service/ServiceMonitor; status/conditions; finalizer stub |
+| 2 | Multi-tenancy | **Done** | `TenantQuota`, validating + mutating webhook, quota enforcement |
+| 3 | Autoscaling | **Done** | Prometheus Adapter integration, managed HPA |
+| 4 | Canary rollout | **Done** | Rollout state machine, Gateway API traffic shifting, PromQL threshold evaluation |
| 5 | MCP registry | Pending | Registrar, registry HTTP handler, discovery API |
| 6 | Hardening & demo | Pending | E2e tests in CI, Helm chart, README, recorded demo |
Phases 2, 3, and 5 are independent of each other and can run in parallel once Phase 1 is complete. Phase 4 depends on both 2 and 3.
+
+### Phase 4 — Canary Rollout (§6)
+
+The canary controller (`internal/rollout.Controller`) is a pure helper driven by the `AgentDeploymentReconciler` each reconcile cycle. When `spec.rollout.strategy: Canary` and `spec.image` differs from `status.stableVersion`, the reconciler transitions to `RolloutInProgress` and calls `Step()` on every subsequent reconcile.
+
+**State machine**: `setWeight` steps create the canary Deployment and upsert a Gateway API `HTTPRoute` with the target weight split. `pause` steps query Prometheus for request count (sample gate), error rate, and p99 latency via `internal/metrics.Client`. If Prometheus is unreachable for longer than a fixed 60-second timeout, a fail-safe rollback fires. Sample-size gating extends the pause window up to the lesser of `3×pause_duration` or an absolute 15-minute maximum before forcing evaluation. Promotion updates the stable Deployment image and restores the HPA; rollback reverts everything and sets `phase=RolloutFailed`.
+
+**New operator flags**: `--prometheus-url` (required for Canary), `--gateway-name`, `--gateway-namespace`.
+
+**New status fields**: `canaryStepIndex`, `pauseStartedAt`, `promUnreachableSince` — all persisted so the state machine is re-entrant across operator restarts.
diff --git a/go.mod b/go.mod
index 3ee41c1..550c94d 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
sigs.k8s.io/controller-runtime v0.19.0
+ sigs.k8s.io/gateway-api v1.1.0
)
require (
@@ -22,16 +23,16 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
- github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
- github.com/go-openapi/jsonpointer v0.19.6 // indirect
- github.com/go-openapi/jsonreference v0.20.2 // indirect
- github.com/go-openapi/swag v0.22.4 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.21.0 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -43,7 +44,7 @@ require (
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
- github.com/imdario/mergo v0.3.6 // indirect
+ github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -60,24 +61,25 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
- go.opentelemetry.io/otel v1.28.0 // indirect
+ go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
- go.opentelemetry.io/otel/metric v1.28.0 // indirect
- go.opentelemetry.io/otel/sdk v1.28.0 // indirect
- go.opentelemetry.io/otel/trace v1.28.0 // indirect
+ go.opentelemetry.io/otel/metric v1.34.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.34.0 // indirect
+ go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
- golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
+ golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
- golang.org/x/sys v0.21.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
- golang.org/x/time v0.3.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
@@ -91,7 +93,7 @@ require (
k8s.io/apiserver v0.31.0 // indirect
k8s.io/component-base v0.31.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
- k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
+ k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
diff --git a/go.sum b/go.sum
index 8db3520..6d0342e 100644
--- a/go.sum
+++ b/go.sum
@@ -11,15 +11,14 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
-github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
-github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
+github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
+github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -35,13 +34,12 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
-github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
-github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
-github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
-github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
-github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -66,8 +64,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
-github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
-github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -76,11 +74,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -111,8 +106,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
@@ -121,33 +116,30 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
-go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
-go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
-go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
-go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
-go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
-go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
-go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
-go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -159,8 +151,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
-golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
+golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -179,16 +171,16 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -220,7 +212,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
@@ -237,17 +228,19 @@ k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
-k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
+k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 h1:Q8Z7VlGhcJgBHJHYugJ/K/7iB8a2eSxCyxdVjJp+lLY=
+k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
+sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM=
+sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
\ No newline at end of file
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/internal/controller/agentdeployment_controller.go b/internal/controller/agentdeployment_controller.go
index a3f517d..3bf982c 100644
--- a/internal/controller/agentdeployment_controller.go
+++ b/internal/controller/agentdeployment_controller.go
@@ -36,11 +36,14 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"github.com/go-logr/logr"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
agentraxv1alpha1 "github.com/gitcommitankit/agentrax/api/v1alpha1"
+ "github.com/gitcommitankit/agentrax/internal/rollout"
"github.com/gitcommitankit/agentrax/internal/scaling"
)
@@ -75,6 +78,11 @@ type AgentDeploymentReconciler struct {
// Used when computing quota headroom for HPA max-replicas capping.
GPUResourceName string
+ // CanaryController drives the canary rollout state machine. When nil (i.e.,
+ // --prometheus-url was not supplied), canary strategy is unavailable and
+ // AgentDeployments that request it are treated as Recreate.
+ CanaryController *rollout.Controller
+
// hasServiceMonitorCRD is set once during SetupWithManager and determines
// whether ServiceMonitor reconciliation is attempted at all.
hasServiceMonitorCRD bool
@@ -109,6 +117,7 @@ func (r *AgentDeploymentReconciler) SetDeregister(fn func(ctx context.Context, a
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch
// +kubebuilder:rbac:groups=agentrax.io,resources=tenantquotas,verbs=get;list;watch
+// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;create;update;patch;delete
// Reconcile drives the AgentDeployment's observed state toward its declared spec.
// It creates and self-heals a Deployment, Service, and (when Prometheus Operator is present)
@@ -152,22 +161,51 @@ func (r *AgentDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, nil
}
- // 3. Reconcile child Deployment.
+ // 3. Handle canary rollout lifecycle before reconciling stable Deployment.
+ // During rollout, the stable Deployment image must remain at status.stableVersion,
+ // not spec.image. Canary handling determines which image to use for stable resources.
+ if r.CanaryController != nil {
+ // Manual abort: spec.rollout.abort=true triggers immediate rollback.
+ if isAbortRequested(ad) {
+ if err := r.CanaryController.Rollback(ctx, ad, "ManualAbort", "spec.rollout.abort was set to true"); err != nil {
+ return ctrl.Result{}, fmt.Errorf("aborting canary: %w", err)
+ }
+ return ctrl.Result{}, nil
+ }
+ // Active rollout: step the state machine and return — HPA and status
+ // updates are owned by the canary controller during rollout.
+ if ad.Status.Phase == agentraxv1alpha1.PhaseRolloutInProgress {
+ result, err := r.CanaryController.Step(ctx, ad)
+ if err != nil {
+ return ctrl.Result{}, fmt.Errorf("stepping canary: %w", err)
+ }
+ return result, nil
+ }
+ // New canary: image changed while strategy is Canary.
+ if isCanaryTriggered(ad) {
+ if err := r.startCanary(ctx, ad); err != nil {
+ return ctrl.Result{}, fmt.Errorf("starting canary: %w", err)
+ }
+ return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
+ }
+ }
+
+ // 4. Reconcile child Deployment.
if err := r.reconcileDeployment(ctx, ad); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling deployment: %w", err)
}
- // 4. Reconcile child Service.
+ // 5. Reconcile child Service.
if err := r.reconcileService(ctx, ad); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling service: %w", err)
}
- // 5. Reconcile ServiceMonitor when Prometheus Operator is present.
+ // 6. Reconcile ServiceMonitor when Prometheus Operator is present.
if err := r.reconcileServiceMonitor(ctx, ad); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling servicemonitor: %w", err)
}
- // 6. Reconcile the managed HPA (skip during active canary — Phase 4 owns it).
+ // 7. Reconcile the managed HPA (skip during active canary — Phase 4 owns it).
// reconcileHPA also returns the quota evaluation state so updateStatus can
// write the correct QuotaLimited condition onto the freshly re-fetched object.
hpaResult, qs, err := r.reconcileHPA(ctx, ad)
@@ -175,7 +213,7 @@ func (r *AgentDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, fmt.Errorf("reconciling hpa: %w", err)
}
- // 7. Derive status from the live Deployment and update it — always last.
+ // 8. Derive status from the live Deployment and update it — always last.
// We continue into updateStatus even when hpaResult requests a requeue so
// that the QuotaLimited condition is written in the same reconcile cycle.
// Return the shorter of the two requeue intervals.
@@ -488,7 +526,14 @@ func (r *AgentDeploymentReconciler) updateStatus(ctx context.Context, ad *agentr
dep.Status.AvailableReplicas == replicas
if rolloutComplete {
- latest.Status.Phase = agentraxv1alpha1.PhaseRunning
+ // If a rollout failed, preserve PhaseRolloutFailed until the user changes spec.image or reverts to stable.
+ if latest.Status.Phase == agentraxv1alpha1.PhaseRolloutFailed && latest.Spec.Image != latest.Status.StableVersion {
+ // Retain PhaseRolloutFailed while spec.image is not the stable version.
+ } else {
+ latest.Status.Phase = agentraxv1alpha1.PhaseRunning
+ latest.Status.CanaryVersion = ""
+ apimeta.RemoveStatusCondition(&latest.Status.Conditions, "RolloutFailed")
+ }
// Derive StableVersion from the image the Deployment controller
// applied — not from latest.Spec.Image — so it reflects what is
// actually running, even if the spec was updated again since.
@@ -549,15 +594,19 @@ func (r *AgentDeploymentReconciler) detectImagePullFailure(ctx context.Context,
// ── Desired-state builders ────────────────────────────────────────────────────
// agentLabels returns the canonical label set applied to all resources owned by ad.
+// For stable resources (Deployment, Service), this includes variant=stable.
func agentLabels(ad *agentraxv1alpha1.AgentDeployment) map[string]string {
return map[string]string{
"app.kubernetes.io/name": ad.Name,
"app.kubernetes.io/managed-by": "agentrax",
"agentrax.io/tenant": ad.Spec.TenantRef,
+ "agentrax.io/variant": "stable",
}
}
// desiredDeployment builds the Deployment spec the reconciler wants to exist.
+// During a canary rollout (PhaseRolloutInProgress), the stable Deployment image
+// is frozen at status.stableVersion; otherwise it tracks spec.image.
func (r *AgentDeploymentReconciler) desiredDeployment(ad *agentraxv1alpha1.AgentDeployment) *appsv1.Deployment {
port := ad.Spec.Port
if port == 0 {
@@ -567,6 +616,12 @@ func (r *AgentDeploymentReconciler) desiredDeployment(ad *agentraxv1alpha1.Agent
labels := agentLabels(ad)
replicas := ad.Spec.Replicas.Min
+ // During rollout, freeze the stable Deployment at status.stableVersion.
+ image := ad.Spec.Image
+ if ad.Status.Phase == agentraxv1alpha1.PhaseRolloutInProgress && ad.Status.StableVersion != "" {
+ image = ad.Status.StableVersion
+ }
+
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: ad.Name,
@@ -588,7 +643,7 @@ func (r *AgentDeploymentReconciler) desiredDeployment(ad *agentraxv1alpha1.Agent
// Use a fixed container name that is always DNS-1035 compliant.
// The AgentDeployment name is used at the object level, not container level.
Name: "agent",
- Image: ad.Spec.Image,
+ Image: image,
Ports: []corev1.ContainerPort{
{
Name: "agent",
@@ -656,12 +711,13 @@ func (r *AgentDeploymentReconciler) desiredServiceMonitor(ad *agentraxv1alpha1.A
Selector: metav1.LabelSelector{
MatchLabels: labels,
},
- // Carry the two labels used by the HPA ExternalMetric selector into
- // every scraped sample. Prometheus will sanitize the dots to underscores
- // during ingestion (app.kubernetes.io/name → app_kubernetes_io_name).
+ // Carry labels into every scraped sample. Prometheus will sanitize the dots
+ // to underscores during ingestion (app.kubernetes.io/name → app_kubernetes_io_name,
+ // agentrax.io/variant → agentrax_io_variant).
TargetLabels: []string{
"app.kubernetes.io/name",
"app.kubernetes.io/managed-by",
+ "agentrax.io/variant",
},
Endpoints: []monitoringv1.Endpoint{
{
@@ -677,6 +733,8 @@ func (r *AgentDeploymentReconciler) desiredServiceMonitor(ad *agentraxv1alpha1.A
// It uses an uncached API reader to check once whether the ServiceMonitor CRD is
// installed, stores the result on the reconciler, and conditionally adds an
// Owns watch for ServiceMonitor so that out-of-band deletions trigger a reconcile.
+// When CanaryController is non-nil it also watches HTTPRoute objects owned by this
+// controller so out-of-band HTTPRoute deletions trigger a reconcile.
func (r *AgentDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Check CRD presence once at startup using the uncached reader so we don't
// require apiextensionsv1 to be registered in the caching informer scheme.
@@ -699,6 +757,79 @@ func (r *AgentDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
if r.hasServiceMonitorCRD {
bldr = bldr.Owns(&monitoringv1.ServiceMonitor{})
}
+ if r.CanaryController != nil {
+ // Watch HTTPRoutes owned by this controller so out-of-band deletions
+ // (e.g. manual kubectl delete) trigger a reconcile and self-heal.
+ bldr = bldr.Owns(&gatewayv1.HTTPRoute{})
+ }
return bldr.Complete(r)
}
+
+// ── Canary helpers ────────────────────────────────────────────────────────────
+
+// isCanaryTriggered returns true when all conditions are met:
+// 1. spec.rollout.strategy == "Canary"
+// 2. status.stableVersion is set (at least one successful deploy has completed)
+// 3. spec.image differs from status.stableVersion
+// 4. status.phase is not already RolloutInProgress
+// 5. status.phase is not RolloutFailed for this exact image (prevents re-trigger loop)
+func isCanaryTriggered(ad *agentraxv1alpha1.AgentDeployment) bool {
+ if ad.Spec.Rollout.Strategy != "Canary" {
+ return false
+ }
+ if ad.Status.StableVersion == "" {
+ // No stable version yet — treat first deploy as Recreate.
+ return false
+ }
+ if ad.Spec.Image == ad.Status.StableVersion {
+ return false
+ }
+ if ad.Status.Phase == agentraxv1alpha1.PhaseRolloutInProgress {
+ return false
+ }
+ if ad.Status.Phase == agentraxv1alpha1.PhaseRolloutFailed && ad.Spec.Image == ad.Status.CanaryVersion {
+ // Do not retry the exact image that just failed or was aborted.
+ return false
+ }
+ return true
+}
+
+// isAbortRequested returns true when spec.rollout.abort is true and a canary
+// rollout is currently in progress. Abort outside of a rollout is ignored.
+func isAbortRequested(ad *agentraxv1alpha1.AgentDeployment) bool {
+ return ad.Spec.Rollout.Abort && ad.Status.Phase == agentraxv1alpha1.PhaseRolloutInProgress
+}
+
+// startCanary transitions an AgentDeployment from its current phase into
+// RolloutInProgress. It records the new canary version in status, resets step
+// tracking fields, and pauses the stable HPA so Phase 4 owns replica counts.
+func (r *AgentDeploymentReconciler) startCanary(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ logger := log.FromContext(ctx).WithValues(
+ "name", ad.Name, "namespace", ad.Namespace,
+ "newImage", ad.Spec.Image, "stableImage", ad.Status.StableVersion,
+ )
+ logger.Info("starting canary rollout")
+
+ // 1. Pause the stable HPA so the canary controller owns replica counts.
+ if err := r.CanaryController.PauseHPA(ctx, ad); err != nil {
+ return fmt.Errorf("pausing HPA before canary: %w", err)
+ }
+
+ // 2. Record canary state in status.
+ latest := &agentraxv1alpha1.AgentDeployment{}
+ if err := r.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, latest); err != nil {
+ return fmt.Errorf("re-fetching AD to start canary: %w", err)
+ }
+ latest.Status.Phase = agentraxv1alpha1.PhaseRolloutInProgress
+ latest.Status.CanaryVersion = ad.Spec.Image
+ latest.Status.CanaryStepIndex = 0
+ latest.Status.CanaryWeight = 0
+ latest.Status.PauseStartedAt = nil
+ latest.Status.PromUnreachableSince = nil
+ if err := r.Status().Update(ctx, latest); err != nil {
+ return fmt.Errorf("updating status to RolloutInProgress: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
index f58d907..adc0d32 100644
--- a/internal/controller/suite_test.go
+++ b/internal/controller/suite_test.go
@@ -41,6 +41,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
agentraxv1alpha1 "github.com/gitcommitankit/agentrax/api/v1alpha1"
"github.com/gitcommitankit/agentrax/internal/quota"
@@ -119,6 +120,8 @@ var _ = BeforeSuite(func() {
// Register apiextensions types so serviceMonitorCRDExists can decode CRD objects
// when called from SetupWithManager via the uncached API reader.
Expect(apiextensionsv1.AddToScheme(scheme.Scheme)).To(Succeed())
+ // Register Gateway API types so HTTPRoute objects can be created in canary tests.
+ Expect(gatewayv1.Install(scheme.Scheme)).To(Succeed())
// +kubebuilder:scaffold:scheme
diff --git a/internal/rollout/canary.go b/internal/rollout/canary.go
new file mode 100644
index 0000000..698cfd8
--- /dev/null
+++ b/internal/rollout/canary.go
@@ -0,0 +1,781 @@
+/*
+Copyright 2026.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package rollout
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ appsv1 "k8s.io/api/apps/v1"
+ autoscalingv2 "k8s.io/api/autoscaling/v2"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/equality"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/intstr"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+
+ agentraxv1alpha1 "github.com/gitcommitankit/agentrax/api/v1alpha1"
+ "github.com/gitcommitankit/agentrax/internal/metrics"
+ "github.com/gitcommitankit/agentrax/internal/scaling"
+)
+
+const (
+ // canaryVariantLabel is the pod label that distinguishes canary pods from stable pods.
+ canaryVariantLabel = "agentrax.io/variant"
+ // canaryVariantValue is the value of canaryVariantLabel for canary pods.
+ canaryVariantValue = "canary"
+
+ // maxPauseExtensionMultiplier is the maximum total pause duration as a multiple
+ // of the configured pause duration. After this ceiling the evaluator runs regardless
+ // of sample size.
+ maxPauseExtensionMultiplier = 3
+
+ // canaryDeploymentSuffix is appended to the AgentDeployment name to form
+ // the canary Deployment name, e.g. "my-agent-canary".
+ canaryDeploymentSuffix = "-canary"
+)
+
+// Controller manages the canary lifecycle for one AgentDeployment.
+// It is a pure helper — it does not implement reconcile.Reconciler.
+// The AgentDeploymentReconciler calls Step() each reconcile cycle when
+// status.phase == RolloutInProgress, and Rollback() when abort is requested.
+type Controller struct {
+ // Client is the controller-runtime client for Kubernetes API access.
+ Client client.Client
+ // Scheme is required by controllerutil.SetControllerReference.
+ Scheme *runtime.Scheme
+ // PromClient is the Prometheus query client used for threshold evaluation.
+ PromClient *metrics.Client
+ // GatewayName is the name of the Gateway API Gateway object.
+ GatewayName string
+ // GatewayNamespace is the namespace of the Gateway API Gateway object.
+ GatewayNamespace string
+ // FailSafeTimeout is how long Prometheus must be unreachable before
+ // an automatic rollback fires.
+ FailSafeTimeout time.Duration
+}
+
+// Step advances the canary state machine by one cycle.
+// It is called every reconcile when status.phase == RolloutInProgress.
+// The returned ctrl.Result tells the reconciler when to requeue.
+func (c *Controller) Step(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) (ctrl.Result, error) {
+ logger := log.FromContext(ctx).WithValues(
+ "name", ad.Name, "namespace", ad.Namespace,
+ "canaryStepIndex", ad.Status.CanaryStepIndex,
+ )
+
+ // Self-heal canary resources (Deployment, Service, and HTTPRoute) if they drift or are deleted out-of-band.
+ if ad.Status.CanaryVersion != "" {
+ if err := c.ensureCanaryDeployment(ctx, ad); err != nil {
+ return ctrl.Result{}, fmt.Errorf("self-healing canary deployment: %w", err)
+ }
+ if err := c.ensureCanaryService(ctx, ad); err != nil {
+ return ctrl.Result{}, fmt.Errorf("self-healing canary service: %w", err)
+ }
+ }
+ if ad.Status.CanaryWeight > 0 {
+ if err := c.ensureHTTPRoute(ctx, ad, 100-ad.Status.CanaryWeight, ad.Status.CanaryWeight); err != nil {
+ return ctrl.Result{}, fmt.Errorf("self-healing httproute: %w", err)
+ }
+ }
+
+ steps := ad.Spec.Rollout.Steps
+ idx := ad.Status.CanaryStepIndex
+
+ if idx >= len(steps) {
+ // All steps completed — this should only happen if Step is called
+ // after promotion was already triggered. Return without action.
+ logger.Info("all canary steps already executed; promoting")
+ return ctrl.Result{}, c.promote(ctx, ad)
+ }
+
+ step := steps[idx]
+
+ switch {
+ case step.SetWeight != nil:
+ return c.executeSetWeight(ctx, ad, idx, *step.SetWeight)
+ case step.Pause != nil:
+ return c.executePause(ctx, ad, idx, step.Pause.Duration)
+ default:
+ // Malformed step — webhook should have rejected this; fail safe.
+ return ctrl.Result{}, c.Rollback(ctx, ad, "InvalidStep",
+ fmt.Sprintf("step %d has neither setWeight nor pause", idx))
+ }
+}
+
+// Rollback triggers an immediate rollback of the canary, restoring the stable image.
+// It may be called by Step (on threshold breach / fail-safe) or directly by the
+// reconciler (on spec.rollout.abort).
+func (c *Controller) Rollback(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment, reason, message string) error {
+ logger := log.FromContext(ctx).WithValues("name", ad.Name, "namespace", ad.Namespace)
+ logger.Info("rolling back canary", "reason", reason, "message", message)
+
+ // 1. Restore stable Deployment image to status.stableVersion.
+ if ad.Status.StableVersion != "" {
+ dep := &appsv1.Deployment{}
+ err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, dep)
+ if err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("fetching stable deployment for rollback: %w", err)
+ }
+ if err == nil && len(dep.Spec.Template.Spec.Containers) > 0 {
+ dep.Spec.Template.Spec.Containers[0].Image = ad.Status.StableVersion
+ if err := c.Client.Update(ctx, dep); err != nil {
+ return fmt.Errorf("restoring stable deployment image during rollback: %w", err)
+ }
+ }
+ }
+
+ // 2. Delete canary Deployment.
+ if err := c.deleteCanaryDeployment(ctx, ad); err != nil {
+ return fmt.Errorf("deleting canary deployment during rollback: %w", err)
+ }
+
+ // 3. Delete canary Service.
+ if err := c.deleteCanaryService(ctx, ad); err != nil {
+ return fmt.Errorf("deleting canary service during rollback: %w", err)
+ }
+
+ // 4. Reset HTTPRoute to 100% stable, then delete it.
+ // Deleting the HTTPRoute is cleaner than leaving it at 100%; the stable
+ // Service continues to receive all traffic directly from the parent Gateway.
+ if err := c.deleteHTTPRoute(ctx, ad); err != nil {
+ return fmt.Errorf("deleting httproute during rollback: %w", err)
+ }
+
+ // 5. Restore stable HPA.
+ if err := c.restoreHPA(ctx, ad); err != nil {
+ return fmt.Errorf("restoring HPA during rollback: %w", err)
+ }
+
+ // 6. Update status.
+ latest := &agentraxv1alpha1.AgentDeployment{}
+ if err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, latest); err != nil {
+ return fmt.Errorf("re-fetching AD for rollback status update: %w", err)
+ }
+ latest.Status.Phase = agentraxv1alpha1.PhaseRolloutFailed
+ // Preserve CanaryVersion so the reconciler remembers which image failed
+ // and does not immediately re-trigger a canary for the same image.
+ if latest.Status.CanaryVersion == "" {
+ latest.Status.CanaryVersion = ad.Spec.Image
+ }
+ latest.Status.CanaryWeight = 0
+ latest.Status.CanaryStepIndex = 0
+ latest.Status.PauseStartedAt = nil
+ latest.Status.PromUnreachableSince = nil
+
+ apimeta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{
+ Type: "RolloutFailed",
+ Status: metav1.ConditionTrue,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: latest.Generation,
+ })
+ apimeta.RemoveStatusCondition(&latest.Status.Conditions, agentraxv1alpha1.ConditionSampleInsufficient)
+
+ if err := c.Client.Status().Update(ctx, latest); err != nil {
+ return fmt.Errorf("updating status after rollback: %w", err)
+ }
+
+ return nil
+}
+
+// ── SetWeight step ────────────────────────────────────────────────────────────
+
+// executeSetWeight applies a traffic-weight step: creates the canary Deployment
+// if needed, upserts the HTTPRoute with the target weight split, updates
+// status.canaryWeight, and advances to the next step.
+func (c *Controller) executeSetWeight(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment, idx int, weight int32) (ctrl.Result, error) {
+ logger := log.FromContext(ctx).WithValues("name", ad.Name, "step", idx, "weight", weight)
+ logger.Info("executing setWeight step")
+
+ // Ensure the canary Deployment is running with the new image.
+ if err := c.ensureCanaryDeployment(ctx, ad); err != nil {
+ return ctrl.Result{}, fmt.Errorf("ensuring canary deployment (step %d): %w", idx, err)
+ }
+
+ // Ensure the canary Service exists.
+ if err := c.ensureCanaryService(ctx, ad); err != nil {
+ return ctrl.Result{}, fmt.Errorf("ensuring canary service (step %d): %w", idx, err)
+ }
+
+ stableWeight := int32(100) - weight
+
+ // Upsert the HTTPRoute with the target traffic split.
+ if err := c.ensureHTTPRoute(ctx, ad, stableWeight, weight); err != nil {
+ return ctrl.Result{}, fmt.Errorf("ensuring httproute (step %d): %w", idx, err)
+ }
+
+ // Update status fields: advance step, record new weight.
+ latest := &agentraxv1alpha1.AgentDeployment{}
+ if err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, latest); err != nil {
+ return ctrl.Result{}, fmt.Errorf("re-fetching AD after setWeight: %w", err)
+ }
+ latest.Status.CanaryWeight = weight
+ latest.Status.CanaryStepIndex = idx + 1
+ latest.Status.PauseStartedAt = nil // clear any stale pause time
+ if err := c.Client.Status().Update(ctx, latest); err != nil {
+ return ctrl.Result{}, fmt.Errorf("updating status after setWeight: %w", err)
+ }
+
+ // If the new step index is a setWeight:100 that was the last step, promote
+ // immediately on the next reconcile (requeue after 1 second).
+ return ctrl.Result{RequeueAfter: time.Second}, nil
+}
+
+// ── Pause step ────────────────────────────────────────────────────────────────
+
+// executePause waits for the pause duration to elapse while monitoring canary
+// metrics. It may:
+// - extend the pause if the sample size is insufficient (up to maxPauseExtensionMultiplier × duration)
+// - trigger a rollback if thresholds are breached
+// - trigger a rollback if Prometheus is unreachable for > FailSafeTimeout
+// - advance to the next step when the pause duration elapses and thresholds are OK
+func (c *Controller) executePause(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment, idx int, duration time.Duration) (ctrl.Result, error) {
+ logger := log.FromContext(ctx).WithValues("name", ad.Name, "step", idx, "duration", duration)
+
+ now := metav1.Now()
+
+ // Record start time on first entry.
+ latest := &agentraxv1alpha1.AgentDeployment{}
+ if err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, latest); err != nil {
+ return ctrl.Result{}, fmt.Errorf("fetching AD in pause step: %w", err)
+ }
+
+ if latest.Status.PauseStartedAt == nil {
+ logger.Info("starting pause window")
+ latest.Status.PauseStartedAt = &now
+ if err := c.Client.Status().Update(ctx, latest); err != nil {
+ return ctrl.Result{}, fmt.Errorf("recording pause start time: %w", err)
+ }
+ // Requeue after the pause duration so we wake up to evaluate.
+ return ctrl.Result{RequeueAfter: duration}, nil
+ }
+
+ elapsed := now.Time.Sub(latest.Status.PauseStartedAt.Time)
+ maxWait := time.Duration(maxPauseExtensionMultiplier) * duration
+ if maxWait > 15*time.Minute {
+ maxWait = 15 * time.Minute
+ }
+
+ // ── Evaluate thresholds ───────────────────────────────────────────────────
+ result, evalErr := Evaluate(ctx, c.PromClient, latest, duration)
+ if evalErr != nil {
+ logger.Error(evalErr, "Prometheus unreachable during pause evaluation")
+ // Mark when Prometheus first became unreachable.
+ if latest.Status.PromUnreachableSince == nil {
+ latest.Status.PromUnreachableSince = &now
+ if updateErr := c.Client.Status().Update(ctx, latest); updateErr != nil {
+ return ctrl.Result{}, fmt.Errorf("recording prom unreachable time: %w", updateErr)
+ }
+ }
+ // Fire fail-safe rollback if Prometheus has been down too long.
+ unreachableDuration := now.Time.Sub(latest.Status.PromUnreachableSince.Time)
+ if unreachableDuration >= c.FailSafeTimeout {
+ return ctrl.Result{}, c.Rollback(ctx, latest, "PrometheusUnavailable",
+ fmt.Sprintf("Prometheus unreachable for %s (fail-safe timeout: %s)", unreachableDuration, c.FailSafeTimeout))
+ }
+ // Not yet at fail-safe timeout; requeue in 10s and keep waiting.
+ return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
+ }
+
+ // Prometheus is reachable — clear any stale unreachable timestamp.
+ if latest.Status.PromUnreachableSince != nil {
+ latest.Status.PromUnreachableSince = nil
+ if err := c.Client.Status().Update(ctx, latest); err != nil {
+ return ctrl.Result{}, fmt.Errorf("clearing prom unreachable time: %w", err)
+ }
+ }
+
+ // ── Threshold breach → rollback ───────────────────────────────────────────
+ if result.ThresholdBreached {
+ logger.Info("threshold breached — rolling back", "reason", result.BreachReason)
+ return ctrl.Result{}, c.Rollback(ctx, latest, "ThresholdBreached", result.BreachReason)
+ }
+
+ // ── Sample too small ──────────────────────────────────────────────────────
+ if result.SampleTooSmall {
+ apimeta.SetStatusCondition(&latest.Status.Conditions, metav1.Condition{
+ Type: agentraxv1alpha1.ConditionSampleInsufficient,
+ Status: metav1.ConditionTrue,
+ Reason: "InsufficientSample",
+ Message: fmt.Sprintf("request count %.0f below minimum %.0f; extending pause", result.SampleCount, float64(latest.Spec.Rollout.Rollback.MinRequestSample)),
+ ObservedGeneration: latest.Generation,
+ })
+ if err := c.Client.Status().Update(ctx, latest); err != nil {
+ return ctrl.Result{}, fmt.Errorf("setting SampleInsufficient condition: %w", err)
+ }
+ // If we are still within the max extension window, wait longer.
+ if elapsed < maxWait {
+ remaining := duration - (elapsed % duration)
+ logger.Info("extending pause due to insufficient sample", "elapsed", elapsed, "maxWait", maxWait, "nextCheck", remaining)
+ return ctrl.Result{RequeueAfter: remaining}, nil
+ }
+ // Max extensions exhausted — force evaluation (fall through to advancement below).
+ logger.Info("max pause extensions exhausted; advancing despite small sample")
+ }
+
+ // ── Pause duration elapsed and thresholds OK ──────────────────────────────
+ if elapsed < duration {
+ // Requeue for the remainder of the pause.
+ remaining := duration - elapsed
+ return ctrl.Result{RequeueAfter: remaining}, nil
+ }
+
+ // Advance to next step.
+ logger.Info("pause complete; advancing to next step")
+ apimeta.RemoveStatusCondition(&latest.Status.Conditions, agentraxv1alpha1.ConditionSampleInsufficient)
+ latest.Status.CanaryStepIndex = idx + 1
+ latest.Status.PauseStartedAt = nil
+ if err := c.Client.Status().Update(ctx, latest); err != nil {
+ return ctrl.Result{}, fmt.Errorf("advancing step index after pause: %w", err)
+ }
+
+ return ctrl.Result{RequeueAfter: time.Second}, nil
+}
+
+// ── Promotion ─────────────────────────────────────────────────────────────────
+
+// promote promotes the canary to stable: updates the stable Deployment image,
+// deletes the canary Deployment and HTTPRoute, restores the HPA, and clears
+// all canary status fields.
+func (c *Controller) promote(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ logger := log.FromContext(ctx).WithValues("name", ad.Name, "namespace", ad.Namespace)
+ logger.Info("promoting canary to stable", "newImage", ad.Status.CanaryVersion)
+
+ // 0. Guard against empty CanaryVersion.
+ if ad.Status.CanaryVersion == "" {
+ return fmt.Errorf("cannot promote: canaryVersion is empty")
+ }
+
+ // 1. Update stable Deployment image.
+ dep := &appsv1.Deployment{}
+ if err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, dep); err != nil {
+ return fmt.Errorf("fetching stable deployment for promotion: %w", err)
+ }
+ if len(dep.Spec.Template.Spec.Containers) > 0 {
+ dep.Spec.Template.Spec.Containers[0].Image = ad.Status.CanaryVersion
+ }
+ if err := c.Client.Update(ctx, dep); err != nil {
+ return fmt.Errorf("updating stable deployment image during promotion: %w", err)
+ }
+
+ // 2. Delete canary Deployment.
+ if err := c.deleteCanaryDeployment(ctx, ad); err != nil {
+ return fmt.Errorf("deleting canary deployment during promotion: %w", err)
+ }
+
+ // 3. Delete canary Service.
+ if err := c.deleteCanaryService(ctx, ad); err != nil {
+ return fmt.Errorf("deleting canary service during promotion: %w", err)
+ }
+
+ // 4. Delete HTTPRoute (traffic fully on stable Service again).
+ if err := c.deleteHTTPRoute(ctx, ad); err != nil {
+ return fmt.Errorf("deleting httproute during promotion: %w", err)
+ }
+
+ // 5. Restore HPA.
+ if err := c.restoreHPA(ctx, ad); err != nil {
+ return fmt.Errorf("restoring HPA during promotion: %w", err)
+ }
+
+ // 6. Update status.
+ latest := &agentraxv1alpha1.AgentDeployment{}
+ if err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, latest); err != nil {
+ return fmt.Errorf("re-fetching AD for promotion status update: %w", err)
+ }
+ promoted := ad.Status.CanaryVersion
+ latest.Status.StableVersion = promoted
+ latest.Status.CanaryVersion = ""
+ latest.Status.CanaryWeight = 0
+ latest.Status.CanaryStepIndex = 0
+ latest.Status.PauseStartedAt = nil
+ latest.Status.PromUnreachableSince = nil
+ latest.Status.Phase = agentraxv1alpha1.PhaseRunning
+
+ apimeta.RemoveStatusCondition(&latest.Status.Conditions, "RolloutFailed")
+ apimeta.RemoveStatusCondition(&latest.Status.Conditions, agentraxv1alpha1.ConditionSampleInsufficient)
+
+ if err := c.Client.Status().Update(ctx, latest); err != nil {
+ return fmt.Errorf("updating status after promotion: %w", err)
+ }
+
+ logger.Info("canary promoted successfully", "stableVersion", promoted)
+ return nil
+}
+
+// ── Canary Deployment ─────────────────────────────────────────────────────────
+
+// ensureCanaryDeployment creates or updates the canary Deployment (-canary)
+// with the new image from spec. The canary Deployment is pinned at 1 replica
+// and is not managed by HPA — the goal is to get representative traffic samples,
+// not to scale the canary independently.
+func (c *Controller) ensureCanaryDeployment(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ canaryName := ad.Name + canaryDeploymentSuffix
+ desired := c.desiredCanaryDeployment(ad, canaryName)
+
+ existing := &appsv1.Deployment{}
+ existing.Name = canaryName
+ existing.Namespace = ad.Namespace
+
+ _, err := controllerutil.CreateOrUpdate(ctx, c.Client, existing, func() error {
+ existing.Labels = desired.Labels
+
+ // Preserve the existing immutable selector on update; only set it on create
+ if existing.ResourceVersion == "" {
+ existing.Spec.Selector = desired.Spec.Selector
+ }
+
+ // Reconcile the complete pod template
+ existing.Spec.Replicas = desired.Spec.Replicas
+ existing.Spec.Template.Labels = desired.Spec.Template.Labels
+ existing.Spec.Template.Spec = desired.Spec.Template.Spec
+
+ if err := controllerutil.SetControllerReference(ad, existing, c.Scheme); err != nil {
+ return fmt.Errorf("setting controller reference: %w", err)
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("reconciling canary deployment: %w", err)
+ }
+ return nil
+}
+
+// desiredCanaryDeployment builds the spec for the canary Deployment.
+func (c *Controller) desiredCanaryDeployment(ad *agentraxv1alpha1.AgentDeployment, name string) *appsv1.Deployment {
+ one := int32(1)
+ port := ad.Spec.Port
+ if port == 0 {
+ port = 8080
+ }
+ labels := map[string]string{
+ "app.kubernetes.io/name": ad.Name,
+ "app.kubernetes.io/managed-by": "agentrax",
+ "agentrax.io/tenant": ad.Spec.TenantRef,
+ canaryVariantLabel: canaryVariantValue,
+ }
+
+ return &appsv1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: ad.Namespace,
+ Labels: labels,
+ },
+ Spec: appsv1.DeploymentSpec{
+ // Pinned at 1 replica — not HPA-managed.
+ Replicas: &one,
+ Selector: &metav1.LabelSelector{MatchLabels: labels},
+ Template: corev1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{Labels: labels},
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{{
+ Name: "agent",
+ Image: ad.Status.CanaryVersion,
+ Ports: []corev1.ContainerPort{{ContainerPort: port, Protocol: corev1.ProtocolTCP}},
+ Resources: ad.Spec.Resources,
+ Env: ad.Spec.Env,
+ Args: ad.Spec.Args,
+ }},
+ },
+ },
+ },
+ }
+}
+
+// deleteCanaryDeployment removes the canary Deployment. Not-found is tolerated.
+func (c *Controller) deleteCanaryDeployment(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ dep := &appsv1.Deployment{}
+ canaryName := ad.Name + canaryDeploymentSuffix
+ err := c.Client.Get(ctx, types.NamespacedName{Name: canaryName, Namespace: ad.Namespace}, dep)
+ if apierrors.IsNotFound(err) {
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("getting canary deployment for deletion: %w", err)
+ }
+ if err := c.Client.Delete(ctx, dep); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("deleting canary deployment: %w", err)
+ }
+ return nil
+}
+
+// ── Canary Service ────────────────────────────────────────────────────────────
+
+// ensureCanaryService creates or updates the canary Service (-canary)
+// with a selector that targets only canary pods (variant=canary label).
+func (c *Controller) ensureCanaryService(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ canaryName := ad.Name + canaryDeploymentSuffix
+ desired := c.desiredCanaryService(ad, canaryName)
+
+ existing := &corev1.Service{}
+ existing.Name = canaryName
+ existing.Namespace = ad.Namespace
+
+ _, err := controllerutil.CreateOrUpdate(ctx, c.Client, existing, func() error {
+ existing.Labels = desired.Labels
+ existing.Spec.Selector = desired.Spec.Selector
+ existing.Spec.Ports = desired.Spec.Ports
+ existing.Spec.Type = desired.Spec.Type
+
+ if err := controllerutil.SetControllerReference(ad, existing, c.Scheme); err != nil {
+ return fmt.Errorf("setting controller reference: %w", err)
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("reconciling canary service: %w", err)
+ }
+ return nil
+}
+
+// desiredCanaryService builds the spec for the canary Service.
+func (c *Controller) desiredCanaryService(ad *agentraxv1alpha1.AgentDeployment, name string) *corev1.Service {
+ port := ad.Spec.Port
+ if port == 0 {
+ port = 8080
+ }
+ labels := map[string]string{
+ "app.kubernetes.io/name": ad.Name,
+ "app.kubernetes.io/managed-by": "agentrax",
+ "agentrax.io/tenant": ad.Spec.TenantRef,
+ canaryVariantLabel: canaryVariantValue,
+ }
+
+ return &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: ad.Namespace,
+ Labels: labels,
+ },
+ Spec: corev1.ServiceSpec{
+ Selector: labels,
+ Ports: []corev1.ServicePort{
+ {
+ Name: "agent",
+ Port: port,
+ TargetPort: intstr.FromInt32(port),
+ Protocol: corev1.ProtocolTCP,
+ },
+ },
+ Type: corev1.ServiceTypeClusterIP,
+ },
+ }
+}
+
+// deleteCanaryService removes the canary Service. Not-found is tolerated.
+func (c *Controller) deleteCanaryService(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ svc := &corev1.Service{}
+ canaryName := ad.Name + canaryDeploymentSuffix
+ err := c.Client.Get(ctx, types.NamespacedName{Name: canaryName, Namespace: ad.Namespace}, svc)
+ if apierrors.IsNotFound(err) {
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("getting canary service for deletion: %w", err)
+ }
+ if err := c.Client.Delete(ctx, svc); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("deleting canary service: %w", err)
+ }
+ return nil
+}
+
+// ── HTTPRoute ─────────────────────────────────────────────────────────────────
+
+// ensureHTTPRoute creates or updates an HTTPRoute that splits traffic between
+// the stable and canary Services according to the given weights (0–100).
+// The HTTPRoute forwards all requests matching "/" to both backends, weighted
+// by stableWeight and canaryWeight respectively.
+func (c *Controller) ensureHTTPRoute(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment, stableWeight, canaryWeight int32) error {
+ desired := c.desiredHTTPRoute(ad, stableWeight, canaryWeight)
+
+ existing := &gatewayv1.HTTPRoute{}
+ err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, existing)
+ if apierrors.IsNotFound(err) {
+ if err := controllerutil.SetControllerReference(ad, desired, c.Scheme); err != nil {
+ return fmt.Errorf("setting owner ref on httproute: %w", err)
+ }
+ if err := c.Client.Create(ctx, desired); err != nil {
+ return fmt.Errorf("creating httproute: %w", err)
+ }
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("getting httproute: %w", err)
+ }
+
+ // Set controller reference on existing HTTPRoute if not already set.
+ if err := controllerutil.SetControllerReference(ad, existing, c.Scheme); err != nil {
+ return fmt.Errorf("setting controller reference on existing httproute: %w", err)
+ }
+
+ // Skip update if spec already matches.
+ if equality.Semantic.DeepEqual(existing.Spec, desired.Spec) {
+ return nil
+ }
+
+ // Update weights if they have changed.
+ existing.Spec = desired.Spec
+ if err := c.Client.Update(ctx, existing); err != nil {
+ return fmt.Errorf("updating httproute weights: %w", err)
+ }
+ return nil
+}
+
+// desiredHTTPRoute builds an HTTPRoute that routes traffic between the stable
+// and canary Services by weight.
+func (c *Controller) desiredHTTPRoute(ad *agentraxv1alpha1.AgentDeployment, stableWeight, canaryWeight int32) *gatewayv1.HTTPRoute {
+ nsPtr := gatewayv1.Namespace(c.GatewayNamespace)
+ canaryServiceName := gatewayv1.ObjectName(ad.Name + canaryDeploymentSuffix)
+ stableServiceName := gatewayv1.ObjectName(ad.Name)
+ portNumber := gatewayv1.PortNumber(ad.Spec.Port)
+ if portNumber == 0 {
+ portNumber = 8080
+ }
+ backendKind := gatewayv1.Kind("Service")
+ backendGroup := gatewayv1.Group("")
+ pathType := gatewayv1.PathMatchPathPrefix
+ pathValue := "/"
+
+ return &gatewayv1.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: ad.Name,
+ Namespace: ad.Namespace,
+ Labels: map[string]string{
+ "app.kubernetes.io/name": ad.Name,
+ "app.kubernetes.io/managed-by": "agentrax",
+ },
+ },
+ Spec: gatewayv1.HTTPRouteSpec{
+ CommonRouteSpec: gatewayv1.CommonRouteSpec{
+ ParentRefs: []gatewayv1.ParentReference{{
+ Name: gatewayv1.ObjectName(c.GatewayName),
+ Namespace: &nsPtr,
+ }},
+ },
+ Rules: []gatewayv1.HTTPRouteRule{{
+ Matches: []gatewayv1.HTTPRouteMatch{{
+ Path: &gatewayv1.HTTPPathMatch{
+ Type: &pathType,
+ Value: &pathValue,
+ },
+ }},
+ BackendRefs: []gatewayv1.HTTPBackendRef{
+ {
+ BackendRef: gatewayv1.BackendRef{
+ BackendObjectReference: gatewayv1.BackendObjectReference{
+ Kind: &backendKind,
+ Group: &backendGroup,
+ Name: stableServiceName,
+ Port: &portNumber,
+ },
+ Weight: &stableWeight,
+ },
+ },
+ {
+ BackendRef: gatewayv1.BackendRef{
+ BackendObjectReference: gatewayv1.BackendObjectReference{
+ Kind: &backendKind,
+ Group: &backendGroup,
+ Name: canaryServiceName,
+ Port: &portNumber,
+ },
+ Weight: &canaryWeight,
+ },
+ },
+ },
+ }},
+ },
+ }
+}
+
+// deleteHTTPRoute removes the HTTPRoute for the AgentDeployment. Not-found is tolerated.
+func (c *Controller) deleteHTTPRoute(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ route := &gatewayv1.HTTPRoute{}
+ err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, route)
+ if apierrors.IsNotFound(err) {
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("getting httproute for deletion: %w", err)
+ }
+ if err := c.Client.Delete(ctx, route); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("deleting httproute: %w", err)
+ }
+ return nil
+}
+
+// ── HPA management ────────────────────────────────────────────────────────────
+
+// PauseHPA deletes the AgentDeployment's HPA so the canary controller owns
+// replica counts exclusively during the rollout. The reconciler's reconcileHPA
+// already skips HPA reconciliation when phase == RolloutInProgress, so the HPA
+// will not be recreated until promotion or rollback completes.
+func (c *Controller) PauseHPA(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ hpa := &autoscalingv2.HorizontalPodAutoscaler{}
+ err := c.Client.Get(ctx, types.NamespacedName{Name: ad.Name, Namespace: ad.Namespace}, hpa)
+ if apierrors.IsNotFound(err) {
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("getting HPA for pause: %w", err)
+ }
+ if err := c.Client.Delete(ctx, hpa); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("pausing HPA: %w", err)
+ }
+ return nil
+}
+
+// restoreHPA re-creates the HPA after promotion or rollback using the stable AD spec.
+// It passes headroom=0 as a conservative initial value; the main reconciler will
+// recompute the correct quota headroom on its next cycle and update accordingly.
+func (c *Controller) restoreHPA(ctx context.Context, ad *agentraxv1alpha1.AgentDeployment) error {
+ // headroom=0 is conservative: the main reconciler will fix it on the next cycle.
+ desired := scaling.BuildHPA(ad, 0)
+
+ existing := &autoscalingv2.HorizontalPodAutoscaler{}
+ existing.Name = desired.Name
+ existing.Namespace = desired.Namespace
+
+ _, err := controllerutil.CreateOrUpdate(ctx, c.Client, existing, func() error {
+ existing.Labels = desired.Labels
+ existing.Spec.ScaleTargetRef = desired.Spec.ScaleTargetRef
+ existing.Spec.MinReplicas = desired.Spec.MinReplicas
+ existing.Spec.MaxReplicas = desired.Spec.MaxReplicas
+ existing.Spec.Metrics = desired.Spec.Metrics
+ existing.Spec.Behavior = desired.Spec.Behavior
+
+ if err := controllerutil.SetControllerReference(ad, existing, c.Scheme); err != nil {
+ return fmt.Errorf("setting controller reference: %w", err)
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("restoring HPA: %w", err)
+ }
+ return nil
+}
diff --git a/internal/rollout/canary_test.go b/internal/rollout/canary_test.go
new file mode 100644
index 0000000..9c67404
--- /dev/null
+++ b/internal/rollout/canary_test.go
@@ -0,0 +1,523 @@
+/*
+Copyright 2026.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package rollout
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ appsv1 "k8s.io/api/apps/v1"
+ autoscalingv2 "k8s.io/api/autoscaling/v2"
+ corev1 "k8s.io/api/core/v1"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ agentraxv1alpha1 "github.com/gitcommitankit/agentrax/api/v1alpha1"
+ "github.com/gitcommitankit/agentrax/internal/metrics"
+)
+
+const testCanaryImage = "img:v2"
+
+// ── Controller unit tests ──────────────────────────────────────────────────
+
+// newTestController constructs a rollout.Controller with a fake client initialized with the scheme.
+func newTestController(initObjs ...runtime.Object) (*Controller, client.Client) {
+ s := runtime.NewScheme()
+ _ = agentraxv1alpha1.AddToScheme(s)
+ _ = appsv1.AddToScheme(s)
+ _ = corev1.AddToScheme(s)
+ _ = autoscalingv2.AddToScheme(s)
+ _ = gatewayv1.Install(s)
+
+ b := fake.NewClientBuilder().WithScheme(s).WithStatusSubresource(&agentraxv1alpha1.AgentDeployment{})
+ if len(initObjs) > 0 {
+ b = b.WithRuntimeObjects(initObjs...)
+ }
+ cl := b.Build()
+
+ ctrl := &Controller{
+ Client: cl,
+ Scheme: s,
+ PromClient: metrics.NewClient("http://localhost:9090"),
+ GatewayName: "agentrax-gateway",
+ GatewayNamespace: "agentrax-system",
+ FailSafeTimeout: 60 * time.Second,
+ }
+ return ctrl, cl
+}
+
+// TestController_Step_SetWeight verifies that executing a setWeight step creates the canary
+// Deployment, creates the HTTPRoute with the specified weights, and advances the step index.
+func TestController_Step_SetWeight(t *testing.T) {
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Steps: []agentraxv1alpha1.CanaryStep{
+ {SetWeight: int32Ptr(20)},
+ {Pause: &metav1.Duration{Duration: 30 * time.Second}},
+ },
+ },
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ CanaryStepIndex: 0,
+ },
+ }
+
+ c, cl := newTestController(ad)
+ res, err := c.Step(context.Background(), ad)
+ if err != nil {
+ t.Fatalf("Step failed: %v", err)
+ }
+ if res.RequeueAfter != time.Second {
+ t.Errorf("expected RequeueAfter=1s after setWeight, got %v", res.RequeueAfter)
+ }
+
+ // Verify canary Deployment was created
+ canaryDep := &appsv1.Deployment{}
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent-canary", Namespace: "default"}, canaryDep); err != nil {
+ t.Errorf("canary deployment not found: %v", err)
+ } else if len(canaryDep.Spec.Template.Spec.Containers) == 0 || canaryDep.Spec.Template.Spec.Containers[0].Image != testCanaryImage {
+ t.Errorf("expected canary deployment image img:v2")
+ }
+
+ // Verify HTTPRoute was created with 80/20 weights
+ route := &gatewayv1.HTTPRoute{}
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, route); err != nil {
+ t.Errorf("httproute not found: %v", err)
+ } else {
+ if len(route.Spec.Rules) == 0 || len(route.Spec.Rules[0].BackendRefs) < 2 {
+ t.Errorf("expected 2 backend refs in HTTPRoute")
+ } else {
+ w0 := *route.Spec.Rules[0].BackendRefs[0].Weight
+ w1 := *route.Spec.Rules[0].BackendRefs[1].Weight
+ if w0 != 80 || w1 != 20 {
+ t.Errorf("expected weights (80, 20), got (%d, %d)", w0, w1)
+ }
+ }
+ }
+
+ // Verify status was advanced
+ updated := &agentraxv1alpha1.AgentDeployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updated)
+ if updated.Status.CanaryStepIndex != 1 {
+ t.Errorf("expected CanaryStepIndex=1, got %d", updated.Status.CanaryStepIndex)
+ }
+ if updated.Status.CanaryWeight != 20 {
+ t.Errorf("expected CanaryWeight=20, got %d", updated.Status.CanaryWeight)
+ }
+}
+
+// TestController_Step_SelfHealing verifies that Step recreates canary Deployment and HTTPRoute if deleted out-of-band.
+func TestController_Step_SelfHealing(t *testing.T) {
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Steps: []agentraxv1alpha1.CanaryStep{
+ {SetWeight: int32Ptr(20)},
+ {Pause: &metav1.Duration{Duration: 30 * time.Second}},
+ },
+ },
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ CanaryWeight: 20,
+ CanaryStepIndex: 1, // During pause step
+ },
+ }
+
+ c, cl := newTestController(ad)
+ // Canary deployment and HTTPRoute do NOT exist initially (simulating deletion).
+ _, err := c.Step(context.Background(), ad)
+ if err != nil {
+ t.Fatalf("Step failed during self-healing: %v", err)
+ }
+
+ // Verify both were restored by self-healing
+ canaryDep := &appsv1.Deployment{}
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent-canary", Namespace: "default"}, canaryDep); err != nil {
+ t.Errorf("expected self-healing to recreate canary deployment: %v", err)
+ }
+ route := &gatewayv1.HTTPRoute{}
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, route); err != nil {
+ t.Errorf("expected self-healing to recreate httproute: %v", err)
+ } else {
+ // Validate traffic split is 80/20
+ if len(route.Spec.Rules) == 0 || len(route.Spec.Rules[0].BackendRefs) < 2 {
+ t.Errorf("expected 2 backend refs in restored HTTPRoute")
+ } else {
+ w0 := *route.Spec.Rules[0].BackendRefs[0].Weight
+ w1 := *route.Spec.Rules[0].BackendRefs[1].Weight
+ if w0 != 80 || w1 != 20 {
+ t.Errorf("expected weights (80, 20), got (%d, %d)", w0, w1)
+ }
+ }
+ }
+}
+
+// TestController_Rollback verifies that Rollback tears down canary resources, restores HPA, and sets PhaseRolloutFailed.
+func TestController_Rollback(t *testing.T) {
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Replicas: agentraxv1alpha1.ScalingPolicy{Min: 1, Max: 5, Metric: "queueDepth", Target: 10},
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Abort: true,
+ },
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ CanaryWeight: 20,
+ },
+ }
+
+ canaryDep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "test-agent-canary", Namespace: "default"}}
+ route := &gatewayv1.HTTPRoute{ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"}}
+
+ c, cl := newTestController(ad, canaryDep, route)
+
+ err := c.Rollback(context.Background(), ad, "ManualAbort", "spec.rollout.abort was true")
+ if err != nil {
+ t.Fatalf("Rollback failed: %v", err)
+ }
+
+ // Verify canary deployment and httproute were deleted
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent-canary", Namespace: "default"}, &appsv1.Deployment{}); err == nil {
+ t.Errorf("expected canary deployment to be deleted")
+ }
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, &gatewayv1.HTTPRoute{}); err == nil {
+ t.Errorf("expected httproute to be deleted")
+ }
+
+ // Verify HPA was restored
+ hpa := &autoscalingv2.HorizontalPodAutoscaler{}
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, hpa); err != nil {
+ t.Errorf("expected HPA to be restored: %v", err)
+ }
+
+ // Verify status was updated to RolloutFailed and canaryVersion preserved
+ updated := &agentraxv1alpha1.AgentDeployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updated)
+ if updated.Status.Phase != agentraxv1alpha1.PhaseRolloutFailed {
+ t.Errorf("expected Phase=RolloutFailed, got %s", updated.Status.Phase)
+ }
+ if updated.Status.CanaryVersion != testCanaryImage {
+ t.Errorf("expected CanaryVersion=img:v2 to be preserved, got %s", updated.Status.CanaryVersion)
+ }
+}
+
+// TestController_Promote verifies that promote updates the stable deployment image and cleans up canary resources.
+func TestController_Promote(t *testing.T) {
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Replicas: agentraxv1alpha1.ScalingPolicy{Min: 1, Max: 5, Metric: "queueDepth", Target: 10},
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ },
+ }
+
+ stableDep := &appsv1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: appsv1.DeploymentSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{{Name: "agent", Image: "img:v1"}},
+ },
+ },
+ },
+ }
+ canaryDep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "test-agent-canary", Namespace: "default"}}
+
+ c, cl := newTestController(ad, stableDep, canaryDep)
+
+ err := c.promote(context.Background(), ad)
+ if err != nil {
+ t.Fatalf("promote failed: %v", err)
+ }
+
+ // Verify stable deployment image was updated
+ updatedDep := &appsv1.Deployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updatedDep)
+ if len(updatedDep.Spec.Template.Spec.Containers) == 0 || updatedDep.Spec.Template.Spec.Containers[0].Image != testCanaryImage {
+ t.Errorf("expected stable deployment image img:v2, got %v", updatedDep.Spec.Template.Spec.Containers)
+ }
+
+ // Verify status was updated to Running
+ updatedAD := &agentraxv1alpha1.AgentDeployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updatedAD)
+ if updatedAD.Status.Phase != agentraxv1alpha1.PhaseRunning {
+ t.Errorf("expected Phase=Running after promote, got %s", updatedAD.Status.Phase)
+ }
+}
+
+// TestController_PauseHPA_RestoreHPA verifies pausing and restoring the HPA.
+func TestController_PauseHPA_RestoreHPA(t *testing.T) {
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: "img:v1",
+ TenantRef: "team-a",
+ Replicas: agentraxv1alpha1.ScalingPolicy{Min: 1, Max: 5, Metric: "queueDepth", Target: 10},
+ },
+ }
+ existingHPA := &autoscalingv2.HorizontalPodAutoscaler{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ }
+
+ c, cl := newTestController(ad, existingHPA)
+
+ // Pause HPA
+ if err := c.PauseHPA(context.Background(), ad); err != nil {
+ t.Fatalf("PauseHPA failed: %v", err)
+ }
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, &autoscalingv2.HorizontalPodAutoscaler{}); err == nil {
+ t.Errorf("expected HPA to be deleted during PauseHPA")
+ }
+
+ // Restore HPA
+ if err := c.restoreHPA(context.Background(), ad); err != nil {
+ t.Fatalf("restoreHPA failed: %v", err)
+ }
+ restoredHPA := &autoscalingv2.HorizontalPodAutoscaler{}
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, restoredHPA); err != nil {
+ t.Errorf("expected HPA to be restored: %v", err)
+ }
+}
+
+// TestController_ExecutePause_InitialEntry verifies that the first entry into a pause step records PauseStartedAt and requeues.
+func TestController_ExecutePause_InitialEntry(t *testing.T) {
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Steps: []agentraxv1alpha1.CanaryStep{
+ {Pause: &metav1.Duration{Duration: 30 * time.Second}},
+ },
+ },
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ CanaryStepIndex: 0,
+ },
+ }
+
+ c, cl := newTestController(ad)
+ res, err := c.executePause(context.Background(), ad, 0, 30*time.Second)
+ if err != nil {
+ t.Fatalf("executePause failed: %v", err)
+ }
+ if res.RequeueAfter != 30*time.Second {
+ t.Errorf("expected RequeueAfter=30s, got %v", res.RequeueAfter)
+ }
+
+ updated := &agentraxv1alpha1.AgentDeployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updated)
+ if updated.Status.PauseStartedAt == nil {
+ t.Errorf("expected PauseStartedAt to be recorded")
+ }
+}
+
+// TestController_ExecutePause_Success_Advance verifies that when pause duration has elapsed and metrics pass, the step index advances.
+func TestController_ExecutePause_Success_Advance(t *testing.T) {
+ srv := prometheusServer(t, []float64{200.0, 0.01, 100.0})
+ defer srv.Close()
+
+ past := metav1.NewTime(time.Now().Add(-40 * time.Second))
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Steps: []agentraxv1alpha1.CanaryStep{
+ {Pause: &metav1.Duration{Duration: 30 * time.Second}},
+ {SetWeight: int32Ptr(100)},
+ },
+ Rollback: agentraxv1alpha1.RollbackPolicy{
+ MaxErrorRate: "5%",
+ MaxP99LatencyMs: 500,
+ MinRequestSample: 100,
+ },
+ },
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ CanaryStepIndex: 0,
+ PauseStartedAt: &past,
+ },
+ }
+
+ c, cl := newTestController(ad)
+ c.PromClient = metrics.NewClient(srv.URL)
+
+ res, err := c.executePause(context.Background(), ad, 0, 30*time.Second)
+ if err != nil {
+ t.Fatalf("executePause failed: %v", err)
+ }
+ if res.RequeueAfter != time.Second {
+ t.Errorf("expected RequeueAfter=1s after pause completion to process next step, got %v", res.RequeueAfter)
+ }
+
+ updated := &agentraxv1alpha1.AgentDeployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updated)
+ if updated.Status.CanaryStepIndex != 1 {
+ t.Errorf("expected CanaryStepIndex=1 after advancement, got %d", updated.Status.CanaryStepIndex)
+ }
+ if updated.Status.PauseStartedAt != nil {
+ t.Errorf("expected PauseStartedAt to be reset to nil")
+ }
+}
+
+// TestController_ExecutePause_PrometheusUnreachable_FailSafe verifies that if Prometheus is down past the fail-safe timeout, rollback is triggered.
+func TestController_ExecutePause_PrometheusUnreachable_FailSafe(t *testing.T) {
+ started := metav1.NewTime(time.Now().Add(-100 * time.Second))
+ unreachableSince := metav1.NewTime(time.Now().Add(-70 * time.Second)) // > 60s
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Steps: []agentraxv1alpha1.CanaryStep{
+ {Pause: &metav1.Duration{Duration: 30 * time.Second}},
+ },
+ },
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ CanaryStepIndex: 0,
+ PauseStartedAt: &started,
+ PromUnreachableSince: &unreachableSince,
+ },
+ }
+
+ c, cl := newTestController(ad)
+ c.PromClient = metrics.NewClient("http://127.0.0.1:1") // Unreachable port
+ c.FailSafeTimeout = 60 * time.Second
+
+ _, err := c.executePause(context.Background(), ad, 0, 30*time.Second)
+ if err != nil {
+ t.Fatalf("executePause failed: %v", err)
+ }
+ updated := &agentraxv1alpha1.AgentDeployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updated)
+ if updated.Status.Phase != agentraxv1alpha1.PhaseRolloutFailed {
+ t.Errorf("expected Phase=RolloutFailed after fail-safe timeout, got %s", updated.Status.Phase)
+ }
+}
+
+// TestController_ExecutePause_SampleTooSmall verifies that when sample count is below minRequestSample, the pause is extended and ConditionSampleInsufficient is set.
+func TestController_ExecutePause_SampleTooSmall(t *testing.T) {
+ srv := prometheusServer(t, []float64{50.0, 0.0, 50.0}) // count=50 < min 100
+ defer srv.Close()
+
+ past := metav1.NewTime(time.Now().Add(-10 * time.Second))
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: testCanaryImage,
+ TenantRef: "team-a",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Steps: []agentraxv1alpha1.CanaryStep{
+ {Pause: &metav1.Duration{Duration: 30 * time.Second}},
+ },
+ Rollback: agentraxv1alpha1.RollbackPolicy{
+ MinRequestSample: 100,
+ },
+ },
+ },
+ Status: agentraxv1alpha1.AgentDeploymentStatus{
+ Phase: agentraxv1alpha1.PhaseRolloutInProgress,
+ StableVersion: "img:v1",
+ CanaryVersion: testCanaryImage,
+ CanaryStepIndex: 0,
+ PauseStartedAt: &past,
+ },
+ }
+
+ canaryDep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "test-agent-canary", Namespace: "default"}}
+ c, cl := newTestController(ad, canaryDep)
+ c.PromClient = metrics.NewClient(srv.URL)
+
+ res, err := c.executePause(context.Background(), ad, 0, 30*time.Second)
+ if err != nil {
+ t.Fatalf("executePause failed: %v", err)
+ }
+ if res.RequeueAfter == 0 {
+ t.Errorf("expected non-zero RequeueAfter for pause extension")
+ }
+
+ updated := &agentraxv1alpha1.AgentDeployment{}
+ _ = cl.Get(context.Background(), types.NamespacedName{Name: "test-agent", Namespace: "default"}, updated)
+ cond := apimeta.FindStatusCondition(updated.Status.Conditions, agentraxv1alpha1.ConditionSampleInsufficient)
+ if cond == nil || cond.Status != metav1.ConditionTrue {
+ t.Errorf("expected ConditionSampleInsufficient to be True")
+ }
+
+ // Verify no rollback occurred: phase should remain RolloutInProgress
+ if updated.Status.Phase != agentraxv1alpha1.PhaseRolloutInProgress {
+ t.Errorf("expected Phase=RolloutInProgress, got %s (no rollback should occur)", updated.Status.Phase)
+ }
+ // Verify canary deployment still exists
+ fetchedCanary := &appsv1.Deployment{}
+ if err := cl.Get(context.Background(), types.NamespacedName{Name: "test-agent-canary", Namespace: "default"}, fetchedCanary); err != nil {
+ t.Errorf("expected canary deployment to still exist (no rollback): %v", err)
+ }
+}
+
+func int32Ptr(i int32) *int32 { return &i }
diff --git a/internal/rollout/promql.go b/internal/rollout/promql.go
new file mode 100644
index 0000000..f767f3a
--- /dev/null
+++ b/internal/rollout/promql.go
@@ -0,0 +1,190 @@
+/*
+Copyright 2026.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package rollout implements the canary rollout state machine for AgentDeployment.
+// It drives progressive traffic shifting via Gateway API HTTPRoute, evaluates
+// PromQL-based rollback thresholds, and handles automatic rollback on breach or
+// Prometheus unavailability (fail-safe).
+package rollout
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ agentraxv1alpha1 "github.com/gitcommitankit/agentrax/api/v1alpha1"
+ "github.com/gitcommitankit/agentrax/internal/metrics"
+)
+
+// ── PromQL query templates ────────────────────────────────────────────────────
+
+// requestCountQuery returns a PromQL expression that sums the total number of
+// HTTP requests received by the canary pods over the given window.
+// The label selectors match the canary Deployment's pod labels:
+// - app.kubernetes.io/name=
+// - agentrax.io/variant=canary (propagated from pod labels via ServiceMonitor)
+func requestCountQuery(adName, namespace string, window time.Duration) string {
+ return fmt.Sprintf(
+ `sum(increase(http_requests_total{namespace=%q,app_kubernetes_io_name=%q,agentrax_io_variant="canary"}[%s])) or vector(0)`,
+ namespace, adName, promDuration(window),
+ )
+}
+
+// errorRateQuery returns a PromQL expression computing the fraction of 5xx
+// responses out of total requests for the canary, over the given window.
+// Returns 0 if no requests have been received (safe division).
+func errorRateQuery(adName, namespace string, window time.Duration) string {
+ d := promDuration(window)
+ return fmt.Sprintf(
+ `(sum(increase(http_requests_total{namespace=%q,app_kubernetes_io_name=%q,agentrax_io_variant="canary",code=~"5.."}[%s])) or vector(0))`+
+ ` / on() group_left `+
+ `clamp_min(sum(increase(http_requests_total{namespace=%q,app_kubernetes_io_name=%q,agentrax_io_variant="canary"}[%s])) or vector(0), 1)`,
+ namespace, adName, d,
+ namespace, adName, d,
+ )
+}
+
+// p99LatencyQuery returns a PromQL expression for the 99th-percentile request
+// latency in milliseconds for the canary pods over the given window.
+func p99LatencyQuery(adName, namespace string, window time.Duration) string {
+ return fmt.Sprintf(
+ `histogram_quantile(0.99, sum by (le) (`+
+ `rate(http_request_duration_milliseconds_bucket{namespace=%q,app_kubernetes_io_name=%q,agentrax_io_variant="canary"}[%s])))`,
+ namespace, adName, promDuration(window),
+ )
+}
+
+// promDuration formats a time.Duration into the Prometheus duration string
+// understood by range selectors and the rate()/increase() functions.
+// e.g. 5*time.Minute → "5m0s".
+func promDuration(d time.Duration) string {
+ return d.String()
+}
+
+// ── Evaluation ────────────────────────────────────────────────────────────────
+
+// EvaluationResult holds the outcome of a single canary threshold evaluation cycle.
+type EvaluationResult struct {
+ // SampleCount is the total request count observed in the evaluation window.
+ SampleCount float64
+ // ErrorRate is the fraction of 5xx responses (0.0–1.0).
+ ErrorRate float64
+ // P99LatencyMs is the 99th-percentile latency in milliseconds.
+ P99LatencyMs float64
+ // SampleTooSmall is true when SampleCount < minRequestSample, meaning
+ // threshold evaluation was skipped to avoid false positives.
+ SampleTooSmall bool
+ // ThresholdBreached is true when any rollback threshold was exceeded.
+ ThresholdBreached bool
+ // BreachReason is a human-readable description of the breach, if any.
+ BreachReason string
+}
+
+// Evaluate queries Prometheus for all canary metrics and evaluates them against
+// the rollback policy defined in the AgentDeployment spec.
+//
+// A non-nil error means Prometheus was unreachable or returned a malformed response.
+// The caller is responsible for tracking how long Prometheus has been unreachable
+// and triggering a fail-safe rollback after FailSafeTimeout.
+//
+// When Prometheus is reachable but SampleCount < minRequestSample, EvaluationResult
+// has SampleTooSmall=true and ThresholdBreached=false — the caller should extend
+// the pause rather than act.
+func Evaluate(
+ ctx context.Context,
+ promClient *metrics.Client,
+ ad *agentraxv1alpha1.AgentDeployment,
+ window time.Duration,
+) (EvaluationResult, error) {
+ name := ad.Name
+ ns := ad.Namespace
+ policy := ad.Spec.Rollout.Rollback
+
+ // ── 1. Sample-size gate ───────────────────────────────────────────────────
+ sampleCount, err := promClient.QueryScalar(ctx, requestCountQuery(name, ns, window))
+ if err != nil {
+ return EvaluationResult{}, fmt.Errorf("querying request count: %w", err)
+ }
+
+ minSample := float64(policy.MinRequestSample)
+ if minSample <= 0 {
+ minSample = 100 // conservative default if not configured
+ }
+
+ if sampleCount < minSample {
+ return EvaluationResult{
+ SampleCount: sampleCount,
+ SampleTooSmall: true,
+ }, nil
+ }
+
+ // ── 2. Error rate threshold ───────────────────────────────────────────────
+ errorRate, err := promClient.QueryScalar(ctx, errorRateQuery(name, ns, window))
+ if err != nil {
+ return EvaluationResult{}, fmt.Errorf("querying error rate: %w", err)
+ }
+
+ maxErrorRateStr := policy.MaxErrorRate
+ if maxErrorRateStr == "" {
+ maxErrorRateStr = "1%" // default
+ }
+ maxRate, parseErr := agentraxv1alpha1.ParseErrorRate(maxErrorRateStr)
+ if parseErr != nil {
+ // Misconfigured spec — treat as a breach to fail safe.
+ return EvaluationResult{
+ SampleCount: sampleCount,
+ ErrorRate: errorRate,
+ ThresholdBreached: true,
+ BreachReason: fmt.Sprintf("invalid maxErrorRate %q: %v", maxErrorRateStr, parseErr),
+ }, nil
+ }
+ if errorRate > maxRate {
+ return EvaluationResult{
+ SampleCount: sampleCount,
+ ErrorRate: errorRate,
+ ThresholdBreached: true,
+ BreachReason: fmt.Sprintf("error rate %.2f%% exceeds threshold %.2f%%",
+ errorRate*100, maxRate*100),
+ }, nil
+ }
+
+ // ── 3. p99 latency threshold ──────────────────────────────────────────────
+ p99Ms, err := promClient.QueryScalar(ctx, p99LatencyQuery(name, ns, window))
+ if err != nil {
+ return EvaluationResult{}, fmt.Errorf("querying p99 latency: %w", err)
+ }
+
+ maxP99 := policy.MaxP99LatencyMs
+ if maxP99 <= 0 {
+ maxP99 = 500 // default 500ms
+ }
+ if p99Ms > float64(maxP99) {
+ return EvaluationResult{
+ SampleCount: sampleCount,
+ ErrorRate: errorRate,
+ P99LatencyMs: p99Ms,
+ ThresholdBreached: true,
+ BreachReason: fmt.Sprintf("p99 latency %.1fms exceeds threshold %dms",
+ p99Ms, maxP99),
+ }, nil
+ }
+
+ return EvaluationResult{
+ SampleCount: sampleCount,
+ ErrorRate: errorRate,
+ P99LatencyMs: p99Ms,
+ }, nil
+}
diff --git a/internal/rollout/promql_test.go b/internal/rollout/promql_test.go
new file mode 100644
index 0000000..003a5ae
--- /dev/null
+++ b/internal/rollout/promql_test.go
@@ -0,0 +1,371 @@
+/*
+Copyright 2026.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package rollout
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ agentraxv1alpha1 "github.com/gitcommitankit/agentrax/api/v1alpha1"
+ "github.com/gitcommitankit/agentrax/internal/metrics"
+)
+
+// ── helpers ────────────────────────────────────────────────────────────────────
+
+// prometheusServer creates an httptest.Server that returns fixed Prometheus
+// instant query responses in round-robin sequence. Each successive HTTP request
+// returns the next value from responses. Evaluate calls the Prometheus API
+// exactly three times per cycle: (1) request count, (2) error rate, (3) p99.
+// Pass values in that order.
+func prometheusServer(t *testing.T, responses []float64) *httptest.Server {
+ t.Helper()
+ idx := 0
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ val := 0.0
+ if idx < len(responses) {
+ val = responses[idx]
+ idx++
+ }
+ body := buildVectorResponse(val)
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write(body)
+ }))
+}
+
+// errorServer returns a Prometheus server that always responds with HTTP 500.
+func errorServer() *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ }))
+}
+
+// buildVectorResponse serializes a single-element Prometheus vector response.
+func buildVectorResponse(value float64) []byte {
+ resp := map[string]interface{}{
+ "status": "success",
+ "data": map[string]interface{}{
+ "resultType": "vector",
+ "result": []interface{}{
+ map[string]interface{}{
+ "metric": map[string]interface{}{},
+ "value": []interface{}{1234567890.0, fmt.Sprintf("%g", value)},
+ },
+ },
+ },
+ }
+ b, _ := json.Marshal(resp)
+ return b
+}
+
+// makeAD returns a minimal AgentDeployment for testing.
+func makeAD(name, ns string, maxErrorRate string, maxP99Ms int32) *agentraxv1alpha1.AgentDeployment {
+ return &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: "img:v1",
+ TenantRef: "tq",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ Rollback: agentraxv1alpha1.RollbackPolicy{
+ MaxErrorRate: maxErrorRate,
+ MaxP99LatencyMs: maxP99Ms,
+ MinRequestSample: 100,
+ },
+ },
+ },
+ }
+}
+
+// ── Query template tests ───────────────────────────────────────────────────────
+
+// TestQueryTemplates verifies that the three PromQL query builders produce valid
+// queries that embed the AgentDeployment name, namespace, and window correctly.
+func TestQueryTemplates(t *testing.T) {
+ ad := makeAD("my-agent", "tenant-prod", "1%", 200)
+ window := 2 * time.Minute
+
+ reqQuery := requestCountQuery(ad.Name, ad.Namespace, window)
+ errQuery := errorRateQuery(ad.Name, ad.Namespace, window)
+ p99Query := p99LatencyQuery(ad.Name, ad.Namespace, window)
+
+ for name, q := range map[string]string{
+ "requestCount": reqQuery,
+ "errorRate": errQuery,
+ "p99Latency": p99Query,
+ } {
+ if !strings.Contains(q, `namespace="tenant-prod"`) {
+ t.Errorf("%s query missing namespace selector: %q", name, q)
+ }
+ if !strings.Contains(q, `app_kubernetes_io_name="my-agent"`) {
+ t.Errorf("%s query missing agent name selector: %q", name, q)
+ }
+ if !strings.Contains(q, `agentrax_io_variant="canary"`) {
+ t.Errorf("%s query missing agentrax_io_variant=canary selector: %q", name, q)
+ }
+ if !strings.Contains(q, "[2m0s]") {
+ t.Errorf("%s query missing duration window [2m0s]: %q", name, q)
+ }
+ }
+}
+
+// ── Evaluate tests ─────────────────────────────────────────────────────────────
+
+// TestEvaluate_AllWithinThresholds verifies that when request count is sufficient
+// and both error rate and latency are below configured maxima, Evaluate returns
+// ThresholdBreached=false and SampleTooSmall=false.
+func TestEvaluate_AllWithinThresholds(t *testing.T) {
+ // Sequence: count=200, errorRate=0.005 (0.5%), p99=80ms (0.080s).
+ srv := prometheusServer(t, []float64{200.0, 0.005, 0.080})
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if result.ThresholdBreached {
+ t.Errorf("expected ThresholdBreached=false, got reason: %s", result.BreachReason)
+ }
+ if result.SampleTooSmall {
+ t.Errorf("expected SampleTooSmall=false, got count=%.0f", result.SampleCount)
+ }
+}
+
+// TestEvaluate_SampleTooSmall verifies that when request count is below
+// minRequestSample, SampleTooSmall=true and ThresholdBreached=false (never evaluate).
+func TestEvaluate_SampleTooSmall(t *testing.T) {
+ // Sequence: count=42 (below min 100); error rate and p99 queries should not occur.
+ srv := prometheusServer(t, []float64{42.0})
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !result.SampleTooSmall {
+ t.Errorf("expected SampleTooSmall=true when count(42) < min(100)")
+ }
+ if result.ThresholdBreached {
+ t.Errorf("expected ThresholdBreached=false when sample is too small; got reason: %s", result.BreachReason)
+ }
+ if result.SampleCount != 42.0 {
+ t.Errorf("expected SampleCount=42, got %g", result.SampleCount)
+ }
+}
+
+// TestEvaluate_ErrorRateBreached verifies that an error rate exceeding MaxErrorRate
+// triggers ThresholdBreached=true with an appropriate reason.
+func TestEvaluate_ErrorRateBreached(t *testing.T) {
+ // Sequence: count=500 (sufficient), errorRate=0.03 (3% > 1% max), p99=50ms.
+ srv := prometheusServer(t, []float64{500.0, 0.03, 0.050})
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !result.ThresholdBreached {
+ t.Errorf("expected ThresholdBreached=true for error rate 3%% > 1%%")
+ }
+ if !strings.Contains(result.BreachReason, "error rate") {
+ t.Errorf("expected breach reason to mention error rate; got %q", result.BreachReason)
+ }
+}
+
+// TestEvaluate_P99LatencyBreached verifies that a p99 latency exceeding
+// MaxP99LatencyMs triggers ThresholdBreached=true with an appropriate reason.
+func TestEvaluate_P99LatencyBreached(t *testing.T) {
+ // Sequence: count=500 (sufficient), errorRate=0.001 (0.1%), p99=350ms (> 200ms max).
+ srv := prometheusServer(t, []float64{500.0, 0.001, 350.0})
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !result.ThresholdBreached {
+ t.Errorf("expected ThresholdBreached=true for p99 350ms > 200ms")
+ }
+ if !strings.Contains(result.BreachReason, "p99 latency") {
+ t.Errorf("expected breach reason to mention p99 latency; got %q", result.BreachReason)
+ }
+}
+
+// TestEvaluate_PrometheusUnreachable verifies that a down/erroring Prometheus
+// returns a non-nil error from Evaluate so the caller can start the fail-safe timer.
+func TestEvaluate_PrometheusUnreachable(t *testing.T) {
+ srv := errorServer()
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ _, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+
+ if err == nil {
+ t.Fatal("expected error when Prometheus returns HTTP 500; got nil")
+ }
+}
+
+// TestEvaluate_EmptyVector verifies that an empty Prometheus result vector
+// (no vector elements returned) returns an error from Evaluate.
+func TestEvaluate_EmptyVector(t *testing.T) {
+ // Empty vector response: {"status":"success","data":{"resultType":"vector","result":[]}}
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ body := `{"status":"success","data":{"resultType":"vector","result":[]}}`
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(body))
+ }))
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ _, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+
+ if err == nil {
+ t.Errorf("expected error on empty vector (0 elements), got nil")
+ }
+}
+
+// TestEvaluate_DefaultThresholds verifies that when RollbackPolicy fields are
+// omitted, sensible defaults (1%, 500ms, min 100) apply.
+func TestEvaluate_DefaultThresholds(t *testing.T) {
+ // Count=50 is below default min(100) -> SampleTooSmall.
+ srv := prometheusServer(t, []float64{50.0})
+ defer srv.Close()
+
+ ad := &agentraxv1alpha1.AgentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "agent", Namespace: "ns"},
+ Spec: agentraxv1alpha1.AgentDeploymentSpec{
+ Image: "img:v1",
+ TenantRef: "tq",
+ Rollout: agentraxv1alpha1.RolloutPolicy{
+ Strategy: "Canary",
+ // Rollback policy omitted completely — testing defaults.
+ },
+ },
+ }
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 5*time.Minute)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !result.SampleTooSmall {
+ t.Errorf("expected SampleTooSmall=true when count(50) < default min(100)")
+ }
+}
+
+// TestEvaluate_InvalidMaxErrorRate verifies that a misconfigured MaxErrorRate
+// (not parseable as a percentage) results in ThresholdBreached=true as a
+// fail-safe rather than silently passing.
+func TestEvaluate_InvalidMaxErrorRate(t *testing.T) {
+ // Sequence: count=200 (sufficient), errorRate=0.01 (parsed but config invalid), p99=100ms.
+ srv := prometheusServer(t, []float64{200.0, 0.01, 100.0})
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "not-a-percentage", 500)
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 5*time.Minute)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !result.ThresholdBreached {
+ t.Error("expected ThresholdBreached=true for invalid maxErrorRate (fail-safe)")
+ }
+}
+
+// TestEvaluate_ZeroSample_AbsentSeries verifies that when Prometheus returns 0.0
+// (e.g. from `or vector(0)` on absent series), Evaluate cleanly flags SampleTooSmall
+// without failing or triggering a false breach.
+func TestEvaluate_ZeroSample_AbsentSeries(t *testing.T) {
+ srv := prometheusServer(t, []float64{0.0})
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+ if err != nil {
+ t.Fatalf("unexpected error when series evaluate to 0: %v", err)
+ }
+ if !result.SampleTooSmall {
+ t.Errorf("expected SampleTooSmall=true when count is 0, got %v", result.SampleTooSmall)
+ }
+ if result.ThresholdBreached {
+ t.Errorf("expected ThresholdBreached=false when sample is 0, got %v", result.ThresholdBreached)
+ }
+ if result.SampleCount != 0 {
+ t.Errorf("expected SampleCount=0, got %f", result.SampleCount)
+ }
+}
+
+// TestEvaluate_ZeroErrors_Absent5xxSeries verifies that when request count is sufficient
+// (>= minRequestSample) and 5xx series are absent (evaluating to 0.0 via `or vector(0)`),
+// Evaluate completes with ErrorRate=0.0, SampleTooSmall=false, and ThresholdBreached=false.
+func TestEvaluate_ZeroErrors_Absent5xxSeries(t *testing.T) {
+ // Sequence: count=200 (sufficient), errorRate=0.0 (0% errors / absent 5xx series), p99=80ms.
+ srv := prometheusServer(t, []float64{200.0, 0.0, 80.0})
+ defer srv.Close()
+
+ ad := makeAD("agent", "ns", "1%", 200)
+ result, err := Evaluate(context.Background(), metrics.NewClient(srv.URL), ad, 2*time.Minute)
+ if err != nil {
+ t.Fatalf("unexpected error when 5xx series is absent: %v", err)
+ }
+ if result.SampleTooSmall {
+ t.Errorf("expected SampleTooSmall=false when count is 200, got %v", result.SampleTooSmall)
+ }
+ if result.ThresholdBreached {
+ t.Errorf("expected ThresholdBreached=false when errorRate is 0.0, got %v (reason: %s)", result.ThresholdBreached, result.BreachReason)
+ }
+ if result.ErrorRate != 0.0 {
+ t.Errorf("expected ErrorRate=0.0, got %f", result.ErrorRate)
+ }
+ if result.SampleCount != 200.0 {
+ t.Errorf("expected SampleCount=200.0, got %f", result.SampleCount)
+ }
+}
+
+// ── promDuration helper ───────────────────────────────────────────────────────
+
+// TestPromDuration_Format verifies that promDuration formats durations correctly.
+func TestPromDuration_Format(t *testing.T) {
+ cases := []struct {
+ in time.Duration
+ want string
+ }{
+ {5 * time.Minute, "5m0s"},
+ {time.Hour, "1h0m0s"},
+ {30 * time.Second, "30s"},
+ }
+ for _, tc := range cases {
+ got := promDuration(tc.in)
+ if got != tc.want {
+ t.Errorf("promDuration(%v) = %q, want %q", tc.in, got, tc.want)
+ }
+ }
+}