Skip to content

Commit

Permalink
dataclients/kubernetes/kubernetestest: support getting resource by na…
Browse files Browse the repository at this point in the history
…me (#2937)

This is useful for testing getting endpoints by service name.

For #2476

Signed-off-by: Alexander Yastrebov <[email protected]>
  • Loading branch information
AlexanderYastrebov authored Feb 14, 2024
1 parent ca9abef commit ecb8c0b
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 86 deletions.
78 changes: 49 additions & 29 deletions dataclients/kubernetes/kubernetestest/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kubernetestest
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
Expand Down Expand Up @@ -43,8 +44,9 @@ type api struct {
func NewAPI(o TestAPIOptions, specs ...io.Reader) (*api, error) {
a := &api{
namespaces: make(map[string]namespace),
// see https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-uris
pathRx: regexp.MustCompile(
"(/namespaces/([^/]+))?/(services|ingresses|routegroups|endpointslices|endpoints|secrets)",
"(?:/namespaces/([^/]+))?/(services|ingresses|routegroups|endpointslices|endpoints|secrets)(?:/(.+))?",
),
}

Expand Down Expand Up @@ -178,30 +180,27 @@ func (a *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

ns := a.all
if parts[2] != "" {
ns = a.namespaces[parts[2]]
if parts[1] != "" {
ns = a.namespaces[parts[1]]
}

var b []byte
switch parts[3] {
resourceType, name := parts[2], parts[3]
switch resourceType {
case "services":
b = filterBySelectors(ns.services, parseSelectors(r))
serve(w, r, ns.services, name)
case "ingresses":
b = filterBySelectors(ns.ingresses, parseSelectors(r))
serve(w, r, ns.ingresses, name)
case "routegroups":
b = filterBySelectors(ns.routeGroups, parseSelectors(r))
serve(w, r, ns.routeGroups, name)
case "endpoints":
b = filterBySelectors(ns.endpoints, parseSelectors(r))
serve(w, r, ns.endpoints, name)
case "endpointslices":
b = filterBySelectors(ns.endpointslices, parseSelectors(r))
serve(w, r, ns.endpointslices, name)
case "secrets":
b = filterBySelectors(ns.secrets, parseSelectors(r))
serve(w, r, ns.secrets, name)
default:
w.WriteHeader(http.StatusNotFound)
return
http.Error(w, fmt.Sprintf("unsupported resource type %s", resourceType), http.StatusBadRequest)
}

w.Write(b)
}

// Parses an optional parameter with `label selectors` into a map if present or, if not present, returns nil.
Expand All @@ -220,35 +219,56 @@ func parseSelectors(r *http.Request) map[string]string {
return selectors
}

// Filters all resources that are already set in k8s namespace using the given selectors map.
// All resources are initially set to `namespace` as slices of bytes and for most tests it's not needed to make it any more complex.
// This helper function deserializes resources, finds a metadata with labels in them and check if they have all
// requested labels. If they do, they are returned.
func filterBySelectors(resources []byte, selectors map[string]string) []byte {
if len(selectors) == 0 {
return resources
func serve(w http.ResponseWriter, r *http.Request, resources []byte, name string) {
selectors := parseSelectors(r)
if name == "" && len(selectors) == 0 {
w.Write(resources)
return
}

labels := struct {
itemsMetadata := struct {
Items []struct {
Metadata struct {
Name string `json:"name"`
Labels map[string]string `json:"labels"`
} `json:"metadata"`
} `json:"items"`
}{}

if err := json.Unmarshal(resources, &itemsMetadata); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// every resource but top level is deserialized because we need access to the indexed array
allItems := struct {
Items []interface{} `json:"items"`
}{}

if json.Unmarshal(resources, &labels) != nil || json.Unmarshal(resources, &allItems) != nil {
return resources
if err := json.Unmarshal(resources, &allItems); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// serve named resource if present
if name != "" {
for idx, item := range itemsMetadata.Items {
if item.Metadata.Name == name {
if result, err := json.Marshal(allItems.Items[idx]); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
w.Write(result)
}
return
}
}
w.WriteHeader(http.StatusNotFound)
return
}

// go over each item's label and check if all selectors with their values are present
var filteredItems []interface{}
for idx, item := range labels.Items {
for idx, item := range itemsMetadata.Items {
allMatch := true
for k, v := range selectors {
label, ok := item.Metadata.Labels[k]
Expand All @@ -261,10 +281,10 @@ func filterBySelectors(resources []byte, selectors map[string]string) []byte {

var result []byte
if err := itemsJSON(&result, filteredItems); err != nil {
return resources
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
w.Write(result)
}

return result
}

func initNamespace(kinds map[string][]interface{}) (ns namespace, err error) {
Expand Down
126 changes: 69 additions & 57 deletions dataclients/kubernetes/kubernetestest/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zalando/skipper/dataclients/kubernetes"
)

Expand Down Expand Up @@ -189,6 +191,22 @@ func getJSON(u string, o interface{}) error {
return json.Unmarshal(b, o)
}

func getField(o map[string]interface{}, names ...string) interface{} {
name := names[0]
if f, ok := o[name]; ok {
if len(names) == 1 {
return f
}

if m, ok := f.(map[string]interface{}); ok {
return getField(m, names[1:]...)
} else {
return nil
}
}
return nil
}

func TestTestAPI(t *testing.T) {
kindListSpec, err := os.Open("testdata/kind-list.yaml")
if err != nil {
Expand All @@ -208,11 +226,14 @@ func TestTestAPI(t *testing.T) {
s := httptest.NewServer(a)
defer s.Close()

get := func(uri string, o interface{}) error {
return getJSON(s.URL+uri, o)
get := func(t *testing.T, uri string, o interface{}) {
t.Helper()
err := getJSON(s.URL+uri, o)
require.NoError(t, err)
}

check := func(t *testing.T, data map[string]interface{}, itemsLength int, kind string) {
t.Helper()
items, ok := data["items"].([]interface{})
if !ok || len(items) != itemsLength {
t.Fatalf("failed to get the right number of %s: expected %d, got %d", kind, itemsLength, len(items))
Expand All @@ -224,7 +245,7 @@ func TestTestAPI(t *testing.T) {

resource, ok := items[0].(map[string]interface{})
if !ok {
t.Fatalf("failed to get the right resource: %s", kind)
t.Fatalf("failed to get the right resource: %s", kind)
}
if resource["kind"] != kind {
t.Fatalf("failed to get the right resource: %s != %s", resource["kind"], kind)
Expand All @@ -235,117 +256,108 @@ func TestTestAPI(t *testing.T) {
const namespace = "internal"

var s map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.ServicesNamespaceFmt, namespace), &s); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.ServicesNamespaceFmt, namespace), &s)
check(t, s, 1, "Service")

var i map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.IngressesV1NamespaceFmt, namespace), &i); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.IngressesV1NamespaceFmt, namespace), &i)
check(t, i, 0, "Ingress")

var r map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.RouteGroupsNamespaceFmt, namespace), &r); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.RouteGroupsNamespaceFmt, namespace), &r)
check(t, r, 1, "RouteGroup")

var e map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.EndpointsNamespaceFmt, namespace), &e); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.EndpointsNamespaceFmt, namespace), &e)
check(t, e, 1, "Endpoints")

var eps map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.EndpointSlicesNamespaceFmt, namespace), &eps); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.EndpointSlicesNamespaceFmt, namespace), &eps)
check(t, eps, 1, "EndpointSlice")

var sec map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.SecretsNamespaceFmt, namespace), &sec); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.SecretsNamespaceFmt, namespace), &sec)
check(t, sec, 1, "Secret")
})

t.Run("without namespace", func(t *testing.T) {
var s map[string]interface{}
if err := get(kubernetes.ServicesClusterURI, &s); err != nil {
t.Fatal(err)
}
get(t, kubernetes.ServicesClusterURI, &s)
check(t, s, 3, "Service")

var i map[string]interface{}
if err := get(kubernetes.IngressesV1ClusterURI, &i); err != nil {
t.Fatal(err)
}
get(t, kubernetes.IngressesV1ClusterURI, &i)
check(t, i, 1, "Ingress")

var r map[string]interface{}
if err := get(kubernetes.RouteGroupsClusterURI, &r); err != nil {
t.Fatal(err)
}
get(t, kubernetes.RouteGroupsClusterURI, &r)
check(t, r, 2, "RouteGroup")

var e map[string]interface{}
if err := get(kubernetes.EndpointsClusterURI, &e); err != nil {
t.Fatal(err)
}
get(t, kubernetes.EndpointsClusterURI, &e)
check(t, e, 3, "Endpoints")

var eps map[string]interface{}
if err := get(kubernetes.EndpointSlicesClusterURI, &eps); err != nil {
t.Fatal(err)
}
get(t, kubernetes.EndpointSlicesClusterURI, &eps)
check(t, eps, 3, "EndpointSlice")

var sec map[string]interface{}
if err := get(kubernetes.SecretsClusterURI, &sec); err != nil {
t.Fatal(err)
}
get(t, kubernetes.SecretsClusterURI, &sec)
check(t, sec, 1, "Secret")
})

t.Run("kind: List", func(t *testing.T) {
const namespace = "baz"

var s map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.ServicesNamespaceFmt, namespace), &s); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.ServicesNamespaceFmt, namespace), &s)
check(t, s, 1, "Service")

var i map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.IngressesV1NamespaceFmt, namespace), &i); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.IngressesV1NamespaceFmt, namespace), &i)
check(t, i, 0, "Ingress")

var r map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.RouteGroupsNamespaceFmt, namespace), &r); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.RouteGroupsNamespaceFmt, namespace), &r)
check(t, r, 1, "RouteGroup")

var e map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.EndpointsNamespaceFmt, namespace), &e); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.EndpointsNamespaceFmt, namespace), &e)
check(t, e, 1, "Endpoints")

var eps map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.EndpointSlicesNamespaceFmt, namespace), &eps); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.EndpointSlicesNamespaceFmt, namespace), &eps)
check(t, eps, 1, "EndpointSlice")

var sec map[string]interface{}
if err := get(fmt.Sprintf(kubernetes.SecretsNamespaceFmt, namespace), &sec); err != nil {
t.Fatal(err)
}
get(t, fmt.Sprintf(kubernetes.SecretsNamespaceFmt, namespace), &sec)
check(t, sec, 0, "Secret")
})

t.Run("resource by name", func(t *testing.T) {
const namespace = "internal"

var s map[string]interface{}
get(t, fmt.Sprintf(kubernetes.ServicesNamespaceFmt, namespace)+"/bar", &s)

assert.Equal(t, "Service", getField(s, "kind"))
assert.Equal(t, namespace, getField(s, "metadata", "namespace"))
assert.Equal(t, "bar", getField(s, "metadata", "name"))

var e map[string]interface{}
get(t, fmt.Sprintf(kubernetes.EndpointsNamespaceFmt, namespace)+"/bar", &e)

assert.Equal(t, "Endpoints", getField(e, "kind"))
assert.Equal(t, namespace, getField(e, "metadata", "namespace"))
assert.Equal(t, "bar", getField(e, "metadata", "name"))
})

t.Run("resource name does not exist", func(t *testing.T) {
const namespace = "internal"

var o map[string]interface{}
err := getJSON(s.URL+fmt.Sprintf(kubernetes.ServicesNamespaceFmt, namespace)+"/does-not-exist", &o)

assert.EqualError(t, err, "unexpected status code: 404")
})
}

0 comments on commit ecb8c0b

Please sign in to comment.