diff --git a/dataclients/kubernetes/clusterclient.go b/dataclients/kubernetes/clusterclient.go index f476f7ec26..5ca1e3a6cb 100644 --- a/dataclients/kubernetes/clusterclient.go +++ b/dataclients/kubernetes/clusterclient.go @@ -519,16 +519,13 @@ func (c *clusterClient) loadEndpointSlices() (map[definitions.ResourceID]*skippe // we should delete it, because of eventual consistency // it is actually terminating delete(resEps, address) - } else if ep.Conditions == nil { + } else if ep.Conditions == nil || ep.isReady() { // if conditions are nil then we need to treat is as ready resEps[address] = &skipperEndpoint{ - Address: address, - Zone: ep.Zone, - } - } else if ep.isReady() { - resEps[address] = &skipperEndpoint{ - Address: address, - Zone: ep.Zone, + Address: address, + Zone: ep.Zone, + NodeName: ep.NodeName, + TargetRef: ep.TargetRef, } } } diff --git a/dataclients/kubernetes/clusterstate_test.go b/dataclients/kubernetes/clusterstate_test.go index f7c55e96a8..69cc8bae34 100644 --- a/dataclients/kubernetes/clusterstate_test.go +++ b/dataclients/kubernetes/clusterstate_test.go @@ -23,17 +23,17 @@ func benchmarkCachedEndpoints(b *testing.B, n int) { Subsets: []*subset{ { Addresses: []*address{ - {"192.168.0.1", "node1"}, - {"192.168.0.2", "node2"}, - {"192.168.0.3", "node3"}, - {"192.168.0.4", "node4"}, - {"192.168.0.5", "node5"}, - {"192.168.0.6", "node6"}, - {"192.168.0.7", "node7"}, - {"192.168.0.8", "node8"}, - {"192.168.0.9", "node9"}, - {"192.168.0.10", "node10"}, - {"192.168.0.11", "node11"}, + {IP: "192.168.0.1", NodeName: "node1"}, + {IP: "192.168.0.2", NodeName: "node2"}, + {IP: "192.168.0.3", NodeName: "node3"}, + {IP: "192.168.0.4", NodeName: "node4"}, + {IP: "192.168.0.5", NodeName: "node5"}, + {IP: "192.168.0.6", NodeName: "node6"}, + {IP: "192.168.0.7", NodeName: "node7"}, + {IP: "192.168.0.8", NodeName: "node8"}, + {IP: "192.168.0.9", NodeName: "node9"}, + {IP: "192.168.0.10", NodeName: "node10"}, + {IP: "192.168.0.11", NodeName: "node11"}, }, Ports: []*port{ {"ssh", 22, "TCP"}, diff --git a/dataclients/kubernetes/endpoints.go b/dataclients/kubernetes/endpoints.go index b293b41fed..ed64870b5f 100644 --- a/dataclients/kubernetes/endpoints.go +++ b/dataclients/kubernetes/endpoints.go @@ -98,8 +98,9 @@ type subset struct { } type address struct { - IP string `json:"ip"` - Node string `json:"nodeName"` + IP string `json:"ip"` + NodeName string `json:"nodeName"` + TargetRef *objectReference `json:"targetRef"` } type port struct { diff --git a/dataclients/kubernetes/endpointslices.go b/dataclients/kubernetes/endpointslices.go index 18e460fb87..6e2d41c2af 100644 --- a/dataclients/kubernetes/endpointslices.go +++ b/dataclients/kubernetes/endpointslices.go @@ -16,8 +16,10 @@ type skipperEndpointSlice struct { // Conditions have to be evaluated before creation type skipperEndpoint struct { - Address string - Zone string + Address string + Zone string + NodeName string + TargetRef *objectReference } func (eps *skipperEndpointSlice) getPort(protocol, pName string, pValue int) int { @@ -125,6 +127,10 @@ type EndpointSliceEndpoints struct { // https://kubernetes.io/docs/concepts/services-networking/topology-aware-routing/#safeguards // Zone aware routing will be available if https://github.com/zalando/skipper/issues/1446 is closed. Zone string `json:"zone"` // "eu-central-1c" + // Node hosting this endpoint. This can be used to determine endpoints local to a node. + NodeName string `json:"nodeName"` + // TargetRef is a reference to a Kubernetes object that represents this endpoint. + TargetRef *objectReference `json:"targetRef"` } type endpointsliceCondition struct { diff --git a/dataclients/kubernetes/kube.go b/dataclients/kubernetes/kube.go index 3500c71de0..58eb0dd06f 100644 --- a/dataclients/kubernetes/kube.go +++ b/dataclients/kubernetes/kube.go @@ -18,7 +18,10 @@ import ( "github.com/zalando/skipper/secrets/certregistry" ) -const DefaultLoadBalancerAlgorithm = "roundRobin" +const ( + DefaultLoadBalancerAlgorithm = "roundRobin" + MetadataRouteID = "kube__metadata" +) const ( defaultIngressClass = "skipper" @@ -437,6 +440,10 @@ func (c *Client) loadAndConvert() ([]*eskip.Route, error) { r = append(r, globalRedirectRoute(c.httpsRedirectCode)) } + if false { // TODO: flag + r = append(r, metadataRoute(state)) + } + return r, nil } diff --git a/dataclients/kubernetes/kube_test.go b/dataclients/kubernetes/kube_test.go index c11a8d15a2..4aba035448 100644 --- a/dataclients/kubernetes/kube_test.go +++ b/dataclients/kubernetes/kube_test.go @@ -86,8 +86,8 @@ func testEndpoints(namespace, name string, labels map[string]string, base string for i := 0; i < n; i++ { adr := &address{ - IP: fmt.Sprintf("%s.%d", base, i), - Node: fmt.Sprintf("node-%d", i), + IP: fmt.Sprintf("%s.%d", base, i), + NodeName: fmt.Sprintf("node-%d", i), } s.Addresses = append(s.Addresses, adr) } diff --git a/dataclients/kubernetes/metadataroute.go b/dataclients/kubernetes/metadataroute.go new file mode 100644 index 0000000000..a4c69fcc60 --- /dev/null +++ b/dataclients/kubernetes/metadataroute.go @@ -0,0 +1,61 @@ +package kubernetes + +import ( + "encoding/json" + "strings" + + "github.com/zalando/skipper/eskip" + "github.com/zalando/skipper/predicates" +) + +type kubeRouteMetadata struct { + Addresses []kubeRouteMetadataAddress `json:"addresses"` +} + +type kubeRouteMetadataAddress struct { + Address string `json:"address"` + Zone string `json:"zone"` + NodeName string `json:"nodeName"` + TargetRef *objectReference `json:"targetRef"` +} + +// metadataRoute creates a route with [MetadataRouteID] id that matches no requests and +// contains metadata for each endpoint address used by Ingresses and RouteGroups. +func metadataRoute(s *clusterState) *eskip.Route { + var metadata kubeRouteMetadata + for id := range s.cachedEndpoints { + if s.enableEndpointSlices { + if eps, ok := s.endpointSlices[id.ResourceID]; ok { + for _, ep := range eps.Endpoints { + metadata.Addresses = append(metadata.Addresses, kubeRouteMetadataAddress{ + Address: ep.Address, + Zone: ep.Zone, + NodeName: ep.NodeName, + TargetRef: ep.TargetRef, + }) + } + } + } else { + if ep, ok := s.endpoints[id.ResourceID]; ok { + for _, subset := range ep.Subsets { + for _, addr := range subset.Addresses { + metadata.Addresses = append(metadata.Addresses, kubeRouteMetadataAddress{ + Address: addr.IP, + NodeName: addr.NodeName, + TargetRef: addr.TargetRef, + }) + } + } + } + } + } + + var b strings.Builder + _ = json.NewEncoder(&b).Encode(&metadata) + + return &eskip.Route{ + Id: MetadataRouteID, + Predicates: []*eskip.Predicate{{Name: predicates.FalseName, Args: []any{b.String()}}}, + BackendType: eskip.ShuntBackend, + } +} diff --git a/dataclients/kubernetes/resources.go b/dataclients/kubernetes/resources.go index 3f35b27885..99c51e4356 100644 --- a/dataclients/kubernetes/resources.go +++ b/dataclients/kubernetes/resources.go @@ -24,3 +24,10 @@ type secret struct { type secretList struct { Items []*secret `json:"items"` } + +type objectReference struct { + Kind string `json:"kind"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Uid string `json:"uid"` +}