Files
weave/lb/gateway_api.go
levaliciousandClaude Opus 5.5 7405b51e53
images / images (push) Successful in 6m32s
lb: serve Gateways (Gateway API v1) from the node holding their address
Gateways of a GatewayClass whose controllerName is reweave.levk.dev/gateway
are served like LoadBalancer Services: a Gateway names one IPv4 address in
spec.addresses, and the node holding that address serves all of it and
writes its status.

- TCP listeners forward the oldest TCPRoute attached to them in the kernel,
  through the same DNAT and reply steering as LoadBalancer Services, so the
  backend sees the client's own address.
- HTTP and HTTPS listeners are served by the agent itself on the holder:
  TLS terminated with the listener's certificate (chosen by SNI, swapped in
  place when its Secret changes, HTTP/2 offered), requests routed by the
  attached HTTPRoutes in the order the spec prescribes (hostname, exact
  path, longest prefix, method, headers, query, age, name, rule), and passed
  to a ready endpoint of the backend with X-Forwarded-For built from this
  hop only. RequestRedirect, URLRewrite and both header modifiers are
  implemented; other filters and regular expressions are refused
  (Accepted False, UnsupportedValue). Bodies are streamed, never limited.
  Unresolved backends answer 500, backends without ready endpoints 503,
  requests nothing matches 404. Those ports are let in ahead of the host
  firewall (REWEAVE-LB-IN).

Listeners are checked for protocol, route kinds, certificates (Secrets in
the Gateway's namespace or granted by a ReferenceGrant, and only in the
namespaces the agent may read: --gateway-namespaces, default
gateway-system), conflicts on a port, and ports LoadBalancer Services
already forward. Routes attach by parentRef (sectionName, port),
allowedRoutes (namespaces Same, All or Selector; kinds) and hostname
intersection, with the spec's conditions and reasons.

One writer per status: the holder writes its Gateway's (the first node by
name when nobody holds the address, so failures are reported once), the
first node by name writes GatewayClass and route statuses, keeping other
controllers' route parents. Writes are conditional on the resourceVersion
they were computed from, and a condition keeps its transition time while
its status stands, so an unchanged status is never rewritten.

The types are plain structs for the subset implemented (the upstream module
needs a newer client-go than the fork's); the CRDs are installed apart from
this manifest, which grants the agent read access to the Gateway API kinds,
patch on their status, and Secrets in gateway-system only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 23:51:44 -04:00

416 lines
12 KiB
Go

package lb
import (
"context"
"encoding/json"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
)
// The Gateway API (gateway.networking.k8s.io/v1, as the v1.6.3 standard channel serves it): the subset this agent
// implements, as plain types. The upstream Go module needs a newer client-go than the fork's; the JSON is the
// contract, and fields this agent does not implement are simply not decoded.
const (
GatewayGroup = "gateway.networking.k8s.io"
GatewayVersion = "v1"
// GatewayControllerName is the spec.controllerName of the GatewayClasses this agent implements.
GatewayControllerName = "reweave.levk.dev/gateway"
GatewayClasses = "gatewayclasses"
Gateways = "gateways"
HTTPRoutes = "httproutes"
TCPRoutes = "tcproutes"
ReferenceGrants = "referencegrants"
)
// GatewayGroupVersion of the Gateway API.
var GatewayGroupVersion = schema.GroupVersion{Group: GatewayGroup, Version: GatewayVersion}
type GatewayClass struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GatewayClassSpec `json:"spec"`
Status GatewayClassStatus `json:"status,omitempty"`
}
type GatewayClassSpec struct {
ControllerName string `json:"controllerName"`
}
type GatewayClassStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
type Gateway struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GatewaySpec `json:"spec"`
Status GatewayStatus `json:"status,omitempty"`
}
type GatewaySpec struct {
GatewayClassName string `json:"gatewayClassName"`
Listeners []Listener `json:"listeners"`
Addresses []GatewayAddress `json:"addresses,omitempty"`
}
// GatewayAddress is a spec or status address; Type defaults to IPAddress.
type GatewayAddress struct {
Type *string `json:"type,omitempty"`
Value string `json:"value"`
}
type Listener struct {
Name string `json:"name"`
Hostname *string `json:"hostname,omitempty"`
Port int32 `json:"port"`
Protocol string `json:"protocol"`
TLS *ListenerTLSConfig `json:"tls,omitempty"`
AllowedRoutes *AllowedRoutes `json:"allowedRoutes,omitempty"`
}
type ListenerTLSConfig struct {
Mode *string `json:"mode,omitempty"`
CertificateRefs []SecretObjectReference `json:"certificateRefs,omitempty"`
}
type SecretObjectReference struct {
Group *string `json:"group,omitempty"`
Kind *string `json:"kind,omitempty"`
Name string `json:"name"`
Namespace *string `json:"namespace,omitempty"`
}
type AllowedRoutes struct {
Namespaces *RouteNamespaces `json:"namespaces,omitempty"`
Kinds []RouteGroupKind `json:"kinds,omitempty"`
}
type RouteNamespaces struct {
From *string `json:"from,omitempty"`
Selector *metav1.LabelSelector `json:"selector,omitempty"`
}
type RouteGroupKind struct {
Group *string `json:"group,omitempty"`
Kind string `json:"kind"`
}
type GatewayStatus struct {
Addresses []GatewayAddress `json:"addresses,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
Listeners []ListenerStatus `json:"listeners,omitempty"`
}
type ListenerStatus struct {
Name string `json:"name"`
SupportedKinds []RouteGroupKind `json:"supportedKinds"`
AttachedRoutes int32 `json:"attachedRoutes"`
Conditions []metav1.Condition `json:"conditions"`
}
type ParentReference struct {
Group *string `json:"group,omitempty"`
Kind *string `json:"kind,omitempty"`
Namespace *string `json:"namespace,omitempty"`
Name string `json:"name"`
SectionName *string `json:"sectionName,omitempty"`
Port *int32 `json:"port,omitempty"`
}
type BackendRef struct {
Group *string `json:"group,omitempty"`
Kind *string `json:"kind,omitempty"`
Name string `json:"name"`
Namespace *string `json:"namespace,omitempty"`
Port *int32 `json:"port,omitempty"`
Weight *int32 `json:"weight,omitempty"`
}
type RouteStatus struct {
Parents []RouteParentStatus `json:"parents"`
}
type RouteParentStatus struct {
ParentRef ParentReference `json:"parentRef"`
ControllerName string `json:"controllerName"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
type HTTPRoute struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec HTTPRouteSpec `json:"spec"`
Status RouteStatus `json:"status,omitempty"`
}
type HTTPRouteSpec struct {
ParentRefs []ParentReference `json:"parentRefs,omitempty"`
Hostnames []string `json:"hostnames,omitempty"`
Rules []HTTPRouteRule `json:"rules,omitempty"`
}
type HTTPRouteRule struct {
Matches []HTTPRouteMatch `json:"matches,omitempty"`
Filters []HTTPRouteFilter `json:"filters,omitempty"`
BackendRefs []HTTPBackendRef `json:"backendRefs,omitempty"`
}
type HTTPRouteMatch struct {
Path *HTTPPathMatch `json:"path,omitempty"`
Headers []HTTPHeaderMatch `json:"headers,omitempty"`
QueryParams []HTTPQueryParamMatch `json:"queryParams,omitempty"`
Method *string `json:"method,omitempty"`
}
type HTTPPathMatch struct {
Type *string `json:"type,omitempty"`
Value *string `json:"value,omitempty"`
}
type HTTPHeaderMatch struct {
Type *string `json:"type,omitempty"`
Name string `json:"name"`
Value string `json:"value"`
}
type HTTPQueryParamMatch struct {
Type *string `json:"type,omitempty"`
Name string `json:"name"`
Value string `json:"value"`
}
// HTTPRouteFilter: the filters this agent implements are decoded; for the others only their presence is known
// (through Type), which is enough to refuse them.
type HTTPRouteFilter struct {
Type string `json:"type"`
RequestHeaderModifier *HTTPHeaderFilter `json:"requestHeaderModifier,omitempty"`
ResponseHeaderModifier *HTTPHeaderFilter `json:"responseHeaderModifier,omitempty"`
RequestRedirect *HTTPRequestRedirectFilter `json:"requestRedirect,omitempty"`
URLRewrite *HTTPURLRewriteFilter `json:"urlRewrite,omitempty"`
}
type HTTPHeaderFilter struct {
Set []HTTPHeader `json:"set,omitempty"`
Add []HTTPHeader `json:"add,omitempty"`
Remove []string `json:"remove,omitempty"`
}
type HTTPHeader struct {
Name string `json:"name"`
Value string `json:"value"`
}
type HTTPRequestRedirectFilter struct {
Scheme *string `json:"scheme,omitempty"`
Hostname *string `json:"hostname,omitempty"`
Path *HTTPPathModifier `json:"path,omitempty"`
Port *int32 `json:"port,omitempty"`
StatusCode *int `json:"statusCode,omitempty"`
}
type HTTPURLRewriteFilter struct {
Hostname *string `json:"hostname,omitempty"`
Path *HTTPPathModifier `json:"path,omitempty"`
}
type HTTPPathModifier struct {
Type string `json:"type"`
ReplaceFullPath *string `json:"replaceFullPath,omitempty"`
ReplacePrefixMatch *string `json:"replacePrefixMatch,omitempty"`
}
type HTTPBackendRef struct {
BackendRef `json:",inline"`
Filters []HTTPRouteFilter `json:"filters,omitempty"`
}
type TCPRoute struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TCPRouteSpec `json:"spec"`
Status RouteStatus `json:"status,omitempty"`
}
type TCPRouteSpec struct {
ParentRefs []ParentReference `json:"parentRefs,omitempty"`
Rules []TCPRouteRule `json:"rules,omitempty"`
}
type TCPRouteRule struct {
BackendRefs []BackendRef `json:"backendRefs,omitempty"`
}
type ReferenceGrant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ReferenceGrantSpec `json:"spec"`
}
type ReferenceGrantSpec struct {
From []ReferenceGrantFrom `json:"from"`
To []ReferenceGrantTo `json:"to"`
}
type ReferenceGrantFrom struct {
Group string `json:"group"`
Kind string `json:"kind"`
Namespace string `json:"namespace"`
}
type ReferenceGrantTo struct {
Group string `json:"group"`
Kind string `json:"kind"`
Name *string `json:"name,omitempty"`
}
type GatewayClassList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GatewayClass `json:"items"`
}
type GatewayList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Gateway `json:"items"`
}
type HTTPRouteList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []HTTPRoute `json:"items"`
}
type TCPRouteList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TCPRoute `json:"items"`
}
type ReferenceGrantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ReferenceGrant `json:"items"`
}
// jsonCopy deep-copies through JSON: these types are only what the JSON says, so nothing is lost.
func jsonCopy(in, out interface{}) {
b, err := json.Marshal(in)
if err != nil {
panic(err)
}
if err := json.Unmarshal(b, out); err != nil {
panic(err)
}
}
func (o *GatewayClass) DeepCopyObject() runtime.Object {
out := &GatewayClass{}
jsonCopy(o, out)
return out
}
func (o *Gateway) DeepCopyObject() runtime.Object {
out := &Gateway{}
jsonCopy(o, out)
return out
}
func (o *HTTPRoute) DeepCopyObject() runtime.Object {
out := &HTTPRoute{}
jsonCopy(o, out)
return out
}
func (o *TCPRoute) DeepCopyObject() runtime.Object {
out := &TCPRoute{}
jsonCopy(o, out)
return out
}
func (o *ReferenceGrant) DeepCopyObject() runtime.Object {
out := &ReferenceGrant{}
jsonCopy(o, out)
return out
}
func (o *GatewayClassList) DeepCopyObject() runtime.Object {
out := &GatewayClassList{}
jsonCopy(o, out)
return out
}
func (o *GatewayList) DeepCopyObject() runtime.Object {
out := &GatewayList{}
jsonCopy(o, out)
return out
}
func (o *HTTPRouteList) DeepCopyObject() runtime.Object {
out := &HTTPRouteList{}
jsonCopy(o, out)
return out
}
func (o *TCPRouteList) DeepCopyObject() runtime.Object {
out := &TCPRouteList{}
jsonCopy(o, out)
return out
}
func (o *ReferenceGrantList) DeepCopyObject() runtime.Object {
out := &ReferenceGrantList{}
jsonCopy(o, out)
return out
}
func addGatewayTypes(s *runtime.Scheme) error {
s.AddKnownTypes(GatewayGroupVersion,
&GatewayClass{}, &GatewayClassList{}, &Gateway{}, &GatewayList{}, &HTTPRoute{}, &HTTPRouteList{},
&TCPRoute{}, &TCPRouteList{}, &ReferenceGrant{}, &ReferenceGrantList{})
metav1.AddToGroupVersion(s, GatewayGroupVersion)
return nil
}
// NewGatewayClient is a REST client for the Gateway API kinds this agent reads and writes the status of.
func NewGatewayClient(base *rest.Config) (*rest.RESTClient, error) {
scheme := runtime.NewScheme()
if err := addGatewayTypes(scheme); err != nil {
return nil, err
}
cfg := *base
cfg.GroupVersion = &GatewayGroupVersion
cfg.APIPath = "/apis"
cfg.ContentType = runtime.ContentTypeJSON
cfg.NegotiatedSerializer = serializer.NewCodecFactory(scheme).WithoutConversion()
if cfg.UserAgent == "" {
cfg.UserAgent = rest.DefaultKubernetesUserAgent()
}
return rest.RESTClientFor(&cfg)
}
// patchStatus merge-patches an object's status. resourceVersion, when given, makes it conditional: a status built
// from a stale read fails instead of overwriting what someone else wrote since.
func patchStatus(ctx context.Context, c rest.Interface, resource, namespace, name, resourceVersion string, status interface{}) error {
body := map[string]interface{}{"status": status}
if resourceVersion != "" {
body["metadata"] = map[string]interface{}{"resourceVersion": resourceVersion}
}
patch, err := json.Marshal(body)
if err != nil {
return err
}
req := c.Patch(types.MergePatchType).Resource(resource).Name(name).SubResource("status").Body(patch)
if namespace != "" {
req = req.Namespace(namespace)
}
return req.Do(ctx).Error()
}