tests: add discovery endpoint for Kube Mock (#29843)

This PR adds the support of discovery endpoints to Teleport Kubernetes
Mock api.

The discovery API will be used to build a collection of cluster
CRD namespaced resources supported by the server.

Signed-off-by: Tiago Silva <tiago.silva@goteleport.com>
This commit is contained in:
Tiago Silva 2023-08-01 15:57:39 +01:00 committed by GitHub
parent 5aac6d8bc5
commit fa07f9b5aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 846 additions and 0 deletions

View file

@ -0,0 +1,148 @@
/*
Copyright 2023 Gravitational, Inc.
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 kubeserver
import (
"net/http"
"path/filepath"
"sort"
"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
var teleportRoleList = metav1.List{
TypeMeta: metav1.TypeMeta{
Kind: "TeleportRoleList",
APIVersion: "resources.teleport.dev/v6",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "1231415",
},
Items: []runtime.RawExtension{
{
Object: newTeleportRole("telerole-1", "default"),
},
{
Object: newTeleportRole("telerole-1", "default"),
},
{
Object: newTeleportRole("telerole-2", "default"),
},
{
Object: newTeleportRole("telerole-test", "default"),
},
{
Object: newTeleportRole("telerole-1", "dev"),
},
{
Object: newTeleportRole("telerole-2", "dev"),
},
},
}
func newTeleportRole(name, namespace string) *unstructured.Unstructured {
obj := &unstructured.Unstructured{}
obj.SetKind("TeleportRole")
obj.SetAPIVersion("resources.teleport.dev/v6")
obj.SetName(name)
obj.SetNamespace(namespace)
return obj
}
func (s *KubeMockServer) listTeleportRoles(w http.ResponseWriter, req *http.Request, p httprouter.Params) (any, error) {
items := []runtime.RawExtension{}
namespace := p.ByName("namespace")
filter := func(obj runtime.Object) bool {
objNamespace := obj.(*unstructured.Unstructured).GetNamespace()
return len(namespace) == 0 || namespace == objNamespace
}
for _, obj := range teleportRoleList.Items {
if filter(obj.Object) {
items = append(items, obj)
}
}
return metav1.List{
TypeMeta: metav1.TypeMeta{
Kind: "TeleportRoleList",
APIVersion: "resources.teleport.dev/v6",
},
ListMeta: metav1.ListMeta{
ResourceVersion: "1231415",
},
Items: items,
}, nil
}
func (s *KubeMockServer) getTeleportRole(w http.ResponseWriter, req *http.Request, p httprouter.Params) (any, error) {
namespace := p.ByName("namespace")
name := p.ByName("name")
filter := func(obj runtime.Object) bool {
metaObj := obj.(*unstructured.Unstructured)
return metaObj.GetName() == name && namespace == metaObj.GetNamespace()
}
for _, obj := range teleportRoleList.Items {
if filter(obj.Object) {
return obj.Object, nil
}
}
return nil, trace.NotFound("teleport %q not found", filepath.Join(namespace, name))
}
const (
teleportRoleKind = "TeleportRole"
)
func (s *KubeMockServer) deleteTeleportRole(w http.ResponseWriter, req *http.Request, p httprouter.Params) (any, error) {
namespace := p.ByName("namespace")
name := p.ByName("name")
deleteOpts, err := parseDeleteCollectionBody(req.Body)
if err != nil {
return nil, trace.Wrap(err)
}
reqID := ""
if deleteOpts.Preconditions != nil && deleteOpts.Preconditions.UID != nil {
reqID = string(*deleteOpts.Preconditions.UID)
}
filter := func(obj runtime.Object) bool {
metaObj := obj.(*unstructured.Unstructured)
return metaObj.GetName() == name && namespace == metaObj.GetNamespace()
}
for _, obj := range teleportRoleList.Items {
if filter(obj.Object) {
s.mu.Lock()
s.deletedResources[deletedResource{kind: teleportRoleKind, requestID: reqID}] = append(s.deletedResources[deletedResource{kind: teleportRoleKind, requestID: reqID}], filepath.Join(namespace, name))
s.mu.Unlock()
return obj.Object, nil
}
}
return nil, trace.NotFound("teleportrole %q not found", filepath.Join(namespace, name))
}
func (s *KubeMockServer) DeletedTeleportRoles(reqID string) []string {
s.mu.Lock()
key := deletedResource{kind: teleportRoleKind, requestID: reqID}
deleted := make([]string, len(s.deletedResources[key]))
copy(deleted, s.deletedResources[key])
s.mu.Unlock()
sort.Strings(deleted)
return deleted
}

View file

@ -0,0 +1,12 @@
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.1.131:6443"
}
]
}

View file

@ -0,0 +1,35 @@
{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "resources.teleport.dev/v6",
"resources": [
{
"name": "teleportroles",
"singularName": "teleportrole",
"namespaced": true,
"kind": "TeleportRole",
"verbs": [
"delete",
"deletecollection",
"get",
"list",
"patch",
"create",
"update",
"watch"
],
"storageVersionHash": "eQsgEapFuzY="
},
{
"name": "teleportroles/status",
"singularName": "",
"namespaced": true,
"kind": "TeleportRole",
"verbs": [
"get",
"patch",
"update"
]
}
]
}

View file

@ -0,0 +1,560 @@
{
"kind": "APIResourceList",
"groupVersion": "v1",
"resources": [
{
"name": "bindings",
"singularName": "",
"namespaced": true,
"kind": "Binding",
"verbs": [
"create"
]
},
{
"name": "componentstatuses",
"singularName": "",
"namespaced": false,
"kind": "ComponentStatus",
"verbs": [
"get",
"list"
],
"shortNames": [
"cs"
]
},
{
"name": "configmaps",
"singularName": "",
"namespaced": true,
"kind": "ConfigMap",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"cm"
],
"storageVersionHash": "qFsyl6wFWjQ="
},
{
"name": "endpoints",
"singularName": "",
"namespaced": true,
"kind": "Endpoints",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"ep"
],
"storageVersionHash": "fWeeMqaN/OA="
},
{
"name": "events",
"singularName": "",
"namespaced": true,
"kind": "Event",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"ev"
],
"storageVersionHash": "r2yiGXH7wu8="
},
{
"name": "limitranges",
"singularName": "",
"namespaced": true,
"kind": "LimitRange",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"limits"
],
"storageVersionHash": "EBKMFVe6cwo="
},
{
"name": "namespaces",
"singularName": "",
"namespaced": false,
"kind": "Namespace",
"verbs": [
"create",
"delete",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"ns"
],
"storageVersionHash": "Q3oi5N2YM8M="
},
{
"name": "namespaces/finalize",
"singularName": "",
"namespaced": false,
"kind": "Namespace",
"verbs": [
"update"
]
},
{
"name": "namespaces/status",
"singularName": "",
"namespaced": false,
"kind": "Namespace",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "nodes",
"singularName": "",
"namespaced": false,
"kind": "Node",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"no"
],
"storageVersionHash": "XwShjMxG9Fs="
},
{
"name": "nodes/proxy",
"singularName": "",
"namespaced": false,
"kind": "NodeProxyOptions",
"verbs": [
"create",
"delete",
"get",
"patch",
"update"
]
},
{
"name": "nodes/status",
"singularName": "",
"namespaced": false,
"kind": "Node",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "persistentvolumeclaims",
"singularName": "",
"namespaced": true,
"kind": "PersistentVolumeClaim",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"pvc"
],
"storageVersionHash": "QWTyNDq0dC4="
},
{
"name": "persistentvolumeclaims/status",
"singularName": "",
"namespaced": true,
"kind": "PersistentVolumeClaim",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "persistentvolumes",
"singularName": "",
"namespaced": false,
"kind": "PersistentVolume",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"pv"
],
"storageVersionHash": "HN/zwEC+JgM="
},
{
"name": "persistentvolumes/status",
"singularName": "",
"namespaced": false,
"kind": "PersistentVolume",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "pods",
"singularName": "",
"namespaced": true,
"kind": "Pod",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"po"
],
"categories": [
"all"
],
"storageVersionHash": "xPOwRZ+Yhw8="
},
{
"name": "pods/attach",
"singularName": "",
"namespaced": true,
"kind": "PodAttachOptions",
"verbs": [
"create",
"get"
]
},
{
"name": "pods/binding",
"singularName": "",
"namespaced": true,
"kind": "Binding",
"verbs": [
"create"
]
},
{
"name": "pods/ephemeralcontainers",
"singularName": "",
"namespaced": true,
"kind": "Pod",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "pods/eviction",
"singularName": "",
"namespaced": true,
"group": "policy",
"version": "v1",
"kind": "Eviction",
"verbs": [
"create"
]
},
{
"name": "pods/exec",
"singularName": "",
"namespaced": true,
"kind": "PodExecOptions",
"verbs": [
"create",
"get"
]
},
{
"name": "pods/log",
"singularName": "",
"namespaced": true,
"kind": "Pod",
"verbs": [
"get"
]
},
{
"name": "pods/portforward",
"singularName": "",
"namespaced": true,
"kind": "PodPortForwardOptions",
"verbs": [
"create",
"get"
]
},
{
"name": "pods/proxy",
"singularName": "",
"namespaced": true,
"kind": "PodProxyOptions",
"verbs": [
"create",
"delete",
"get",
"patch",
"update"
]
},
{
"name": "pods/status",
"singularName": "",
"namespaced": true,
"kind": "Pod",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "podtemplates",
"singularName": "",
"namespaced": true,
"kind": "PodTemplate",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"storageVersionHash": "LIXB2x4IFpk="
},
{
"name": "replicationcontrollers",
"singularName": "",
"namespaced": true,
"kind": "ReplicationController",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"rc"
],
"categories": [
"all"
],
"storageVersionHash": "Jond2If31h0="
},
{
"name": "replicationcontrollers/scale",
"singularName": "",
"namespaced": true,
"group": "autoscaling",
"version": "v1",
"kind": "Scale",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "replicationcontrollers/status",
"singularName": "",
"namespaced": true,
"kind": "ReplicationController",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "resourcequotas",
"singularName": "",
"namespaced": true,
"kind": "ResourceQuota",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"quota"
],
"storageVersionHash": "8uhSgffRX6w="
},
{
"name": "resourcequotas/status",
"singularName": "",
"namespaced": true,
"kind": "ResourceQuota",
"verbs": [
"get",
"patch",
"update"
]
},
{
"name": "secrets",
"singularName": "",
"namespaced": true,
"kind": "Secret",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"storageVersionHash": "S6u1pOWzb84="
},
{
"name": "serviceaccounts",
"singularName": "",
"namespaced": true,
"kind": "ServiceAccount",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"sa"
],
"storageVersionHash": "pbx9ZvyFpBE="
},
{
"name": "serviceaccounts/token",
"singularName": "",
"namespaced": true,
"group": "authentication.k8s.io",
"version": "v1",
"kind": "TokenRequest",
"verbs": [
"create"
]
},
{
"name": "services",
"singularName": "",
"namespaced": true,
"kind": "Service",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
],
"shortNames": [
"svc"
],
"categories": [
"all"
],
"storageVersionHash": "0/CO1lhkEBI="
},
{
"name": "services/proxy",
"singularName": "",
"namespaced": true,
"kind": "ServiceProxyOptions",
"verbs": [
"create",
"delete",
"get",
"patch",
"update"
]
},
{
"name": "services/status",
"singularName": "",
"namespaced": true,
"kind": "Service",
"verbs": [
"get",
"patch",
"update"
]
}
]
}

View file

@ -0,0 +1,19 @@
{
"kind": "APIGroupList",
"apiVersion": "v1",
"groups": [
{
"name": "resources.teleport.dev",
"versions": [
{
"groupVersion": "resources.teleport.dev/v6",
"version": "v6"
}
],
"preferredVersion": {
"groupVersion": "resources.teleport.dev/v6",
"version": "v6"
}
}
]
}

View file

@ -0,0 +1,62 @@
/*
Copyright 2023 Gravitational, Inc.
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 kubeserver
import (
_ "embed"
"net/http"
"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
)
var (
//go:embed data/api.json
apiResponse string
//go:embed data/api_v1.json
apiV1Response string
//go:embed data/apis.json
apisResponse string
//go:embed data/api_teleport.json
teleportAPIResponse string
)
const (
apiEndpoint = "/api"
apiV1Endpoint = "/api/v1"
apisEndpoint = "/apis"
teleportAPIEndpoint = "/apis/resources.teleport.dev/v6"
)
func (s *KubeMockServer) discoveryEndpoint(w http.ResponseWriter, req *http.Request, p httprouter.Params) (any, error) {
switch req.URL.Path {
case apiEndpoint:
w.Write([]byte(apiResponse))
return nil, nil
case apiV1Endpoint:
w.Write([]byte(apiV1Response))
return nil, nil
case apisEndpoint:
w.Write([]byte(apisResponse))
return nil, nil
case teleportAPIEndpoint:
w.Write([]byte(teleportAPIResponse))
return nil, nil
default:
return nil, trace.NotFound("path %v is not supported", req.URL.Path)
}
}

View file

@ -151,6 +151,16 @@ func (s *KubeMockServer) setup() {
s.router.DELETE("/api/:ver/namespaces/:namespace/secrets/:name", s.withWriter(s.deleteSecret))
s.router.POST("/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", s.withWriter(s.selfSubjectAccessReviews))
s.router.GET("/resources.teleport.dev/v6/namespaces/:namespace/teleportroles", s.withWriter(s.listTeleportRoles))
s.router.GET("/resources.teleport.dev/v6/teleportroles", s.withWriter(s.listTeleportRoles))
s.router.GET("/resources.teleport.dev/v6/namespaces/:namespace/teleportroles/:name", s.withWriter(s.getTeleportRole))
s.router.DELETE("/resources.teleport.dev/v6/namespaces/:namespace/teleportroles/:name", s.withWriter(s.deleteTeleportRole))
for _, endpoint := range []string{"/api", "/api/:ver", "/apis", "/apis/resources.teleport.dev/v6"} {
s.router.GET(endpoint, s.withWriter(s.discoveryEndpoint))
}
s.server = httptest.NewUnstartedServer(s.router)
s.server.EnableHTTP2 = true
}