Skip to content

Commit

Permalink
dataclients/kubernetes: add metadata route
Browse files Browse the repository at this point in the history
Adds a special route with a predefined id `kube__metadata`
that contains `False` predicate and no filters.

The first argument of `False` predicate (which it ignores anyway)
contains endpoint metadata encoded as JSON.

Example (formatted):
```
kube__metadata: False("{
\"addresses\":[
  {
    \"address\":\"1.2.3.4\",
    \"zone\":\"zoneA\",
    \"nodeName\":\"node1\",
    \"targetRef\":{
      \"kind\":\"Pod\",
      \"name\":\"foo-app\",
      \"namespace\":\"default\",
      \"uid\":\"83a6408f-8a5b-405c-93f4-1199f7712956\"
    }
  },
  ...
]")
```

This route could be used to obtain zone, node and pod names for a given address.
E.g. EndpointRegistry postprocessor may check this route and store zone, node and pod name for each endpoint.

TODO:
- [ ] add flag  to enable metadata route
- [ ] tests

Fixes #1559

Signed-off-by: Alexander Yastrebov <[email protected]>
  • Loading branch information
AlexanderYastrebov committed Jan 11, 2024
1 parent 4d76a05 commit fa772eb
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 26 deletions.
13 changes: 5 additions & 8 deletions dataclients/kubernetes/clusterclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
Expand Down
22 changes: 11 additions & 11 deletions dataclients/kubernetes/clusterstate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
5 changes: 3 additions & 2 deletions dataclients/kubernetes/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions dataclients/kubernetes/endpointslices.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion dataclients/kubernetes/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import (
"github.com/zalando/skipper/secrets/certregistry"
)

const DefaultLoadBalancerAlgorithm = "roundRobin"
const (
DefaultLoadBalancerAlgorithm = "roundRobin"
MetadataRouteID = "kube__metadata"
)

const (
defaultIngressClass = "skipper"
Expand Down Expand Up @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions dataclients/kubernetes/kube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
61 changes: 61 additions & 0 deletions dataclients/kubernetes/metadataroute.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
7 changes: 7 additions & 0 deletions dataclients/kubernetes/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

0 comments on commit fa772eb

Please sign in to comment.