0fc1f17866
This is a partial revert of 288d9c999e
. For some reason it didn't occur to me
that we could do it this way earlier. Whoops.
This also contains a middleware update: mutation funcs can return an error now
and short-circuit the rest of the request/response flow. The idea here is that
if someone is configuring their kubeclient to use middleware, they are agreeing
to a narrow-er client contract by doing so (e.g., their TokenCredentialRequest's
must have an Spec.Authenticator.APIGroup set).
I also updated some internal/groupsuffix tests to be more realistic.
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
899 lines
31 KiB
Go
899 lines
31 KiB
Go
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package kubeclient
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/transport"
|
|
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
|
|
|
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
|
|
configv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1"
|
|
"go.pinniped.dev/internal/testutil/fakekubeapi"
|
|
)
|
|
|
|
const (
|
|
someClusterName = "some cluster name"
|
|
)
|
|
|
|
var (
|
|
podGVK = corev1.SchemeGroupVersion.WithKind("Pod")
|
|
goodPod = &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "good-pod",
|
|
Namespace: "good-namespace",
|
|
},
|
|
}
|
|
|
|
apiServiceGVK = apiregistrationv1.SchemeGroupVersion.WithKind("APIService")
|
|
goodAPIService = &apiregistrationv1.APIService{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "good-api-service",
|
|
},
|
|
}
|
|
|
|
tokenCredentialRequestGVK = loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequest")
|
|
goodTokenCredentialRequest = &loginv1alpha1.TokenCredentialRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "good-token-credential-request",
|
|
Namespace: "good-namespace",
|
|
},
|
|
}
|
|
|
|
federationDomainGVK = configv1alpha1.SchemeGroupVersion.WithKind("FederationDomain")
|
|
goodFederationDomain = &configv1alpha1.FederationDomain{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "good-federation-domain",
|
|
Namespace: "good-namespace",
|
|
},
|
|
}
|
|
|
|
middlewareAnnotations = map[string]string{"some-annotation": "thing 1"}
|
|
middlewareLabels = map[string]string{"some-label": "thing 2"}
|
|
)
|
|
|
|
func TestKubeclient(t *testing.T) {
|
|
// plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug) // uncomment me to get some more debug logs
|
|
|
|
tests := []struct {
|
|
name string
|
|
editRestConfig func(t *testing.T, restConfig *rest.Config)
|
|
middlewares func(t *testing.T) []*spyMiddleware
|
|
reallyRunTest func(t *testing.T, c *Client)
|
|
wantMiddlewareReqs, wantMiddlewareResps [][]Object
|
|
}{
|
|
{
|
|
name: "crud core api",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
// create
|
|
pod, err := c.Kubernetes.
|
|
CoreV1().
|
|
Pods(goodPod.Namespace).
|
|
Create(context.Background(), goodPod, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodPod, pod)
|
|
|
|
// read
|
|
pod, err = c.Kubernetes.
|
|
CoreV1().
|
|
Pods(pod.Namespace).
|
|
Get(context.Background(), pod.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, with(goodPod, annotations(), labels()), pod)
|
|
|
|
// read when not found
|
|
_, err = c.Kubernetes.
|
|
CoreV1().
|
|
Pods(pod.Namespace).
|
|
Get(context.Background(), "this-pod-does-not-exist", metav1.GetOptions{})
|
|
require.EqualError(t, err, `couldn't find object for path "/api/v1/namespaces/good-namespace/pods/this-pod-does-not-exist"`)
|
|
|
|
// update
|
|
goodPodWithAnnotationsAndLabelsAndClusterName := with(goodPod, annotations(), labels(), clusterName()).(*corev1.Pod)
|
|
pod, err = c.Kubernetes.
|
|
CoreV1().
|
|
Pods(pod.Namespace).
|
|
Update(context.Background(), goodPodWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodPodWithAnnotationsAndLabelsAndClusterName, pod)
|
|
|
|
// delete
|
|
err = c.Kubernetes.
|
|
CoreV1().
|
|
Pods(pod.Namespace).
|
|
Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
wantMiddlewareReqs: [][]Object{
|
|
{
|
|
with(goodPod, gvk(podGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
|
|
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
|
|
},
|
|
{
|
|
with(goodPod, annotations(), gvk(podGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
|
|
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(podGVK)),
|
|
},
|
|
},
|
|
wantMiddlewareResps: [][]Object{
|
|
{
|
|
with(goodPod, annotations(), labels(), gvk(podGVK)),
|
|
with(goodPod, annotations(), labels(), gvk(podGVK)),
|
|
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
|
|
},
|
|
{
|
|
with(goodPod, emptyAnnotations(), labels(), gvk(podGVK)),
|
|
with(goodPod, annotations(), labels(), gvk(podGVK)),
|
|
with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "crud core api without middlewares",
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
// create
|
|
pod, err := c.Kubernetes.
|
|
CoreV1().
|
|
Pods(goodPod.Namespace).
|
|
Create(context.Background(), goodPod, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodPod, pod)
|
|
|
|
// read
|
|
pod, err = c.Kubernetes.
|
|
CoreV1().
|
|
Pods(pod.Namespace).
|
|
Get(context.Background(), pod.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, with(goodPod), pod)
|
|
|
|
// update
|
|
pod, err = c.Kubernetes.
|
|
CoreV1().
|
|
Pods(pod.Namespace).
|
|
Update(context.Background(), goodPod, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodPod, pod)
|
|
|
|
// delete
|
|
err = c.Kubernetes.
|
|
CoreV1().
|
|
Pods(pod.Namespace).
|
|
Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
name: "crud aggregation api",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
// create
|
|
apiService, err := c.Aggregation.
|
|
ApiregistrationV1().
|
|
APIServices().
|
|
Create(context.Background(), goodAPIService, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodAPIService, apiService)
|
|
|
|
// read
|
|
apiService, err = c.Aggregation.
|
|
ApiregistrationV1().
|
|
APIServices().
|
|
Get(context.Background(), apiService.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, with(goodAPIService, annotations(), labels()), apiService)
|
|
|
|
// update
|
|
goodAPIServiceWithAnnotationsAndLabelsAndClusterName := with(goodAPIService, annotations(), labels(), clusterName()).(*apiregistrationv1.APIService)
|
|
apiService, err = c.Aggregation.
|
|
ApiregistrationV1().
|
|
APIServices().
|
|
Update(context.Background(), goodAPIServiceWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodAPIServiceWithAnnotationsAndLabelsAndClusterName, apiService)
|
|
|
|
// delete
|
|
err = c.Aggregation.
|
|
ApiregistrationV1().
|
|
APIServices().
|
|
Delete(context.Background(), apiService.Name, metav1.DeleteOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
wantMiddlewareReqs: [][]Object{
|
|
{
|
|
with(goodAPIService, gvk(apiServiceGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
|
|
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
|
|
},
|
|
{
|
|
with(goodAPIService, annotations(), gvk(apiServiceGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
|
|
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)),
|
|
},
|
|
},
|
|
wantMiddlewareResps: [][]Object{
|
|
{
|
|
with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)),
|
|
with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)),
|
|
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
|
|
},
|
|
{
|
|
with(goodAPIService, emptyAnnotations(), labels(), gvk(apiServiceGVK)),
|
|
with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)),
|
|
with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "crud concierge api",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
// create
|
|
tokenCredentialRequest, err := c.PinnipedConcierge.
|
|
LoginV1alpha1().
|
|
TokenCredentialRequests(goodTokenCredentialRequest.Namespace).
|
|
Create(context.Background(), goodTokenCredentialRequest, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodTokenCredentialRequest, tokenCredentialRequest)
|
|
|
|
// read
|
|
tokenCredentialRequest, err = c.PinnipedConcierge.
|
|
LoginV1alpha1().
|
|
TokenCredentialRequests(tokenCredentialRequest.Namespace).
|
|
Get(context.Background(), tokenCredentialRequest.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, with(goodTokenCredentialRequest, annotations(), labels()), tokenCredentialRequest)
|
|
|
|
// update
|
|
goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName := with(goodTokenCredentialRequest, annotations(), labels(), clusterName()).(*loginv1alpha1.TokenCredentialRequest)
|
|
tokenCredentialRequest, err = c.PinnipedConcierge.
|
|
LoginV1alpha1().
|
|
TokenCredentialRequests(tokenCredentialRequest.Namespace).
|
|
Update(context.Background(), goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodTokenCredentialRequestWithAnnotationsAndLabelsAndClusterName, tokenCredentialRequest)
|
|
|
|
// delete
|
|
err = c.PinnipedConcierge.
|
|
LoginV1alpha1().
|
|
TokenCredentialRequests(tokenCredentialRequest.Namespace).
|
|
Delete(context.Background(), tokenCredentialRequest.Name, metav1.DeleteOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
wantMiddlewareReqs: [][]Object{
|
|
{
|
|
with(goodTokenCredentialRequest, gvk(tokenCredentialRequestGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
|
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
|
},
|
|
{
|
|
with(goodTokenCredentialRequest, annotations(), gvk(tokenCredentialRequestGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
|
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(tokenCredentialRequestGVK)),
|
|
},
|
|
},
|
|
wantMiddlewareResps: [][]Object{
|
|
{
|
|
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
|
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
|
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
|
},
|
|
{
|
|
with(goodTokenCredentialRequest, emptyAnnotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
|
with(goodTokenCredentialRequest, annotations(), labels(), gvk(tokenCredentialRequestGVK)),
|
|
with(goodTokenCredentialRequest, annotations(), labels(), clusterName(), gvk(tokenCredentialRequestGVK)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "crud supervisor api",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newAnnotationMiddleware(t), newLabelMiddleware(t)}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
// create
|
|
federationDomain, err := c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(goodFederationDomain.Namespace).
|
|
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodFederationDomain, federationDomain)
|
|
|
|
// read
|
|
federationDomain, err = c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(federationDomain.Namespace).
|
|
Get(context.Background(), federationDomain.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, with(goodFederationDomain, annotations(), labels()), federationDomain)
|
|
|
|
// update
|
|
goodFederationDomainWithAnnotationsAndLabelsAndClusterName := with(goodFederationDomain, annotations(), labels(), clusterName()).(*configv1alpha1.FederationDomain)
|
|
federationDomain, err = c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(federationDomain.Namespace).
|
|
Update(context.Background(), goodFederationDomainWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodFederationDomainWithAnnotationsAndLabelsAndClusterName, federationDomain)
|
|
|
|
// delete
|
|
err = c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(federationDomain.Namespace).
|
|
Delete(context.Background(), federationDomain.Name, metav1.DeleteOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
wantMiddlewareReqs: [][]Object{
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
{
|
|
with(goodFederationDomain, annotations(), gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
},
|
|
wantMiddlewareResps: [][]Object{
|
|
{
|
|
with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
|
|
},
|
|
{
|
|
with(goodFederationDomain, emptyAnnotations(), labels(), gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "we don't call any middleware if there are no mutation funcs",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newSimpleMiddleware(t, false, false, false), newSimpleMiddleware(t, false, false, false)}
|
|
},
|
|
reallyRunTest: createGetFederationDomainTest,
|
|
wantMiddlewareReqs: [][]Object{nil, nil},
|
|
wantMiddlewareResps: [][]Object{nil, nil},
|
|
},
|
|
{
|
|
name: "we don't call any resp middleware if there was no req mutations done and there are no resp mutation funcs",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newSimpleMiddleware(t, true, false, false), newSimpleMiddleware(t, true, false, false)}
|
|
},
|
|
reallyRunTest: createGetFederationDomainTest,
|
|
wantMiddlewareReqs: [][]Object{
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
},
|
|
wantMiddlewareResps: [][]Object{nil, nil},
|
|
},
|
|
{
|
|
name: "we don't call any resp middleware if there are no resp mutation funcs even if there was req mutations done",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newSimpleMiddleware(t, true, true, false), newSimpleMiddleware(t, true, true, false)}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
// create
|
|
federationDomain, err := c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(goodFederationDomain.Namespace).
|
|
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, with(goodFederationDomain, clusterName()), federationDomain)
|
|
|
|
// read
|
|
federationDomain, err = c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(federationDomain.Namespace).
|
|
Get(context.Background(), federationDomain.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, with(goodFederationDomain, clusterName()), federationDomain)
|
|
},
|
|
wantMiddlewareReqs: [][]Object{
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
{
|
|
with(goodFederationDomain, clusterName(), gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
},
|
|
wantMiddlewareResps: [][]Object{nil, nil},
|
|
},
|
|
{
|
|
name: "we still call resp middleware if there is a resp mutation func even if there were req mutation funcs",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newSimpleMiddleware(t, false, false, true), newSimpleMiddleware(t, false, false, true)}
|
|
},
|
|
reallyRunTest: createGetFederationDomainTest,
|
|
wantMiddlewareReqs: [][]Object{nil, nil},
|
|
wantMiddlewareResps: [][]Object{
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
},
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "we still call resp middleware if there is a resp mutation func even if there was no req mutation",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newSimpleMiddleware(t, true, false, true), newSimpleMiddleware(t, true, false, true)}
|
|
},
|
|
reallyRunTest: createGetFederationDomainTest,
|
|
wantMiddlewareReqs: [][]Object{
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)),
|
|
},
|
|
},
|
|
wantMiddlewareResps: [][]Object{
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
},
|
|
{
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
with(goodFederationDomain, gvk(federationDomainGVK)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "mutating object meta on a get request is not allowed since that isn't pertinent to the api request",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{{
|
|
name: "non-pertinent mutater",
|
|
t: t,
|
|
mutateReq: func(rt RoundTrip, obj Object) error {
|
|
clusterName()(obj)
|
|
return nil
|
|
},
|
|
}}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
_, err := c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(goodFederationDomain.Namespace).
|
|
Get(context.Background(), goodFederationDomain.Name, metav1.GetOptions{})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid object meta mutation")
|
|
},
|
|
wantMiddlewareReqs: [][]Object{{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))}},
|
|
wantMiddlewareResps: [][]Object{nil},
|
|
},
|
|
{
|
|
name: "when the client gets errors from the api server",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{newSimpleMiddleware(t, true, false, false)}
|
|
},
|
|
editRestConfig: func(t *testing.T, restConfig *rest.Config) {
|
|
restConfig.Dial = func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
return nil, fmt.Errorf("some fake connection error")
|
|
}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
_, err := c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(goodFederationDomain.Namespace).
|
|
Get(context.Background(), goodFederationDomain.Name, metav1.GetOptions{})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), ": some fake connection error")
|
|
},
|
|
wantMiddlewareReqs: [][]Object{{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))}},
|
|
wantMiddlewareResps: [][]Object{nil},
|
|
},
|
|
{
|
|
name: "when there are request middleware failures, we return an error and don't send the request",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{
|
|
// use 3 middleware to ensure that we collect all errors from all middlewares
|
|
newFailingMiddleware(t, "aaa", true, false),
|
|
newFailingMiddleware(t, "bbb", false, false),
|
|
newFailingMiddleware(t, "ccc", true, false),
|
|
}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
_, err := c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(goodFederationDomain.Namespace).
|
|
Get(context.Background(), goodFederationDomain.Name, metav1.GetOptions{})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), ": request mutation failed: [aaa: request error, ccc: request error]")
|
|
},
|
|
wantMiddlewareReqs: [][]Object{
|
|
{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))},
|
|
{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))},
|
|
{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))},
|
|
},
|
|
wantMiddlewareResps: [][]Object{
|
|
nil,
|
|
nil,
|
|
nil,
|
|
},
|
|
},
|
|
{
|
|
name: "when there are response middleware failures, we return an error",
|
|
middlewares: func(t *testing.T) []*spyMiddleware {
|
|
return []*spyMiddleware{
|
|
// use 3 middleware to ensure that we collect all errors from all middlewares
|
|
newFailingMiddleware(t, "aaa", false, true),
|
|
newFailingMiddleware(t, "bbb", false, false),
|
|
newFailingMiddleware(t, "ccc", false, true),
|
|
}
|
|
},
|
|
reallyRunTest: func(t *testing.T, c *Client) {
|
|
_, err := c.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(goodFederationDomain.Namespace).
|
|
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), ": response mutation failed: [aaa: response error, ccc: response error]")
|
|
},
|
|
wantMiddlewareReqs: [][]Object{
|
|
{with(goodFederationDomain, gvk(federationDomainGVK))},
|
|
{with(goodFederationDomain, gvk(federationDomainGVK))},
|
|
{with(goodFederationDomain, gvk(federationDomainGVK))},
|
|
},
|
|
wantMiddlewareResps: [][]Object{
|
|
{with(goodFederationDomain, gvk(federationDomainGVK))},
|
|
{with(goodFederationDomain, gvk(federationDomainGVK))},
|
|
{with(goodFederationDomain, gvk(federationDomainGVK))},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
server, restConfig := fakekubeapi.Start(t, nil)
|
|
defer server.Close()
|
|
|
|
if test.editRestConfig != nil {
|
|
test.editRestConfig(t, restConfig)
|
|
}
|
|
|
|
var middlewares []*spyMiddleware
|
|
if test.middlewares != nil {
|
|
middlewares = test.middlewares(t)
|
|
}
|
|
|
|
// our rt chain is:
|
|
// wantCloseReq -> kubeclient -> wantCloseResp -> http.DefaultTransport -> wantCloseResp -> kubeclient -> wantCloseReq
|
|
restConfig.Wrap(wantCloseRespWrapper(t))
|
|
opts := []Option{WithConfig(restConfig), WithTransportWrapper(wantCloseReqWrapper(t))}
|
|
for _, middleware := range middlewares {
|
|
opts = append(opts, WithMiddleware(middleware))
|
|
}
|
|
client, err := New(opts...)
|
|
require.NoError(t, err)
|
|
|
|
test.reallyRunTest(t, client)
|
|
|
|
for i, spyMiddleware := range middlewares {
|
|
require.Equalf(t, test.wantMiddlewareReqs[i], spyMiddleware.reqObjs, "unexpected req obj in middleware %q (index %d)", spyMiddleware.name, i)
|
|
require.Equalf(t, test.wantMiddlewareResps[i], spyMiddleware.respObjs, "unexpected resp obj in middleware %q (index %d)", spyMiddleware.name, i)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type spyMiddleware struct {
|
|
name string
|
|
t *testing.T
|
|
mutateReq func(RoundTrip, Object) error
|
|
mutateResp func(RoundTrip, Object) error
|
|
reqObjs []Object
|
|
respObjs []Object
|
|
}
|
|
|
|
func (s *spyMiddleware) Handle(_ context.Context, rt RoundTrip) {
|
|
s.t.Log(s.name, "handling", reqStr(rt, nil))
|
|
|
|
if s.mutateReq != nil {
|
|
rt.MutateRequest(func(obj Object) error {
|
|
s.t.Log(s.name, "mutating request", reqStr(rt, obj))
|
|
s.reqObjs = append(s.reqObjs, obj.DeepCopyObject().(Object))
|
|
return s.mutateReq(rt, obj)
|
|
})
|
|
}
|
|
|
|
if s.mutateResp != nil {
|
|
rt.MutateResponse(func(obj Object) error {
|
|
s.t.Log(s.name, "mutating response", reqStr(rt, obj))
|
|
s.respObjs = append(s.respObjs, obj.DeepCopyObject().(Object))
|
|
return s.mutateResp(rt, obj)
|
|
})
|
|
}
|
|
}
|
|
|
|
func reqStr(rt RoundTrip, obj Object) string {
|
|
b := strings.Builder{}
|
|
fmt.Fprintf(&b, "%s /%s", rt.Verb(), rt.Resource().GroupVersion())
|
|
if rt.NamespaceScoped() {
|
|
fmt.Fprintf(&b, "/namespaces/%s", rt.Namespace())
|
|
}
|
|
fmt.Fprintf(&b, "/%s", rt.Resource().Resource)
|
|
if obj != nil {
|
|
fmt.Fprintf(&b, "/%s", obj.GetName())
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func newAnnotationMiddleware(t *testing.T) *spyMiddleware {
|
|
return &spyMiddleware{
|
|
name: "annotater",
|
|
t: t,
|
|
mutateReq: func(rt RoundTrip, obj Object) error {
|
|
if rt.Verb() == VerbCreate {
|
|
annotations()(obj)
|
|
}
|
|
return nil
|
|
},
|
|
mutateResp: func(rt RoundTrip, obj Object) error {
|
|
if rt.Verb() == VerbCreate {
|
|
for key := range middlewareAnnotations {
|
|
delete(obj.GetAnnotations(), key)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func newLabelMiddleware(t *testing.T) *spyMiddleware {
|
|
return &spyMiddleware{
|
|
name: "labeler",
|
|
t: t,
|
|
mutateReq: func(rt RoundTrip, obj Object) error {
|
|
if rt.Verb() == VerbCreate {
|
|
labels()(obj)
|
|
}
|
|
return nil
|
|
},
|
|
mutateResp: func(rt RoundTrip, obj Object) error {
|
|
if rt.Verb() == VerbCreate {
|
|
for key := range middlewareLabels {
|
|
delete(obj.GetLabels(), key)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func newSimpleMiddleware(t *testing.T, hasMutateReqFunc, mutatedReq, hasMutateRespFunc bool) *spyMiddleware {
|
|
m := &spyMiddleware{
|
|
name: "simple",
|
|
t: t,
|
|
}
|
|
if hasMutateReqFunc {
|
|
m.mutateReq = func(rt RoundTrip, obj Object) error {
|
|
if mutatedReq {
|
|
if rt.Verb() == VerbCreate {
|
|
obj.SetClusterName(someClusterName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
if hasMutateRespFunc {
|
|
m.mutateResp = func(rt RoundTrip, obj Object) error {
|
|
return nil
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func newFailingMiddleware(t *testing.T, name string, mutateReqFails, mutateRespFails bool) *spyMiddleware {
|
|
m := &spyMiddleware{
|
|
name: "failing-middleware-" + name,
|
|
t: t,
|
|
}
|
|
|
|
m.mutateReq = func(rt RoundTrip, obj Object) error {
|
|
if mutateReqFails {
|
|
return fmt.Errorf("%s: request error", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
m.mutateResp = func(rt RoundTrip, obj Object) error {
|
|
if mutateRespFails {
|
|
return fmt.Errorf("%s: response error", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
type wantCloser struct {
|
|
io.ReadCloser
|
|
closeCount int
|
|
closeCalls []string
|
|
couldReadBytesJustBeforeClosing bool
|
|
}
|
|
|
|
func (wc *wantCloser) Close() error {
|
|
wc.closeCount++
|
|
wc.closeCalls = append(wc.closeCalls, getCaller())
|
|
n, _ := wc.ReadCloser.Read([]byte{0})
|
|
if n > 0 {
|
|
// there were still bytes left to be read
|
|
wc.couldReadBytesJustBeforeClosing = true
|
|
}
|
|
return wc.ReadCloser.Close()
|
|
}
|
|
|
|
func getCaller() string {
|
|
_, file, line, ok := runtime.Caller(2)
|
|
if !ok {
|
|
file = "???"
|
|
line = 0
|
|
}
|
|
return fmt.Sprintf("%s:%d", file, line)
|
|
}
|
|
|
|
// wantCloseReqWrapper returns a transport.WrapperFunc that validates that the http.Request
|
|
// passed to the underlying http.RoundTripper is closed properly.
|
|
func wantCloseReqWrapper(t *testing.T) transport.WrapperFunc {
|
|
caller := getCaller()
|
|
return func(rt http.RoundTripper) http.RoundTripper {
|
|
return roundTripperFunc(func(req *http.Request) (bool, *http.Response, error) {
|
|
if req.Body != nil {
|
|
wc := &wantCloser{ReadCloser: req.Body}
|
|
t.Cleanup(func() {
|
|
require.Equalf(t, wc.closeCount, 1, "did not close req body expected number of times at %s for req %#v; actual calls = %s", caller, req, wc.closeCalls)
|
|
})
|
|
req.Body = wc
|
|
}
|
|
|
|
if req.GetBody != nil {
|
|
originalBodyCopy, originalErr := req.GetBody()
|
|
req.GetBody = func() (io.ReadCloser, error) {
|
|
if originalErr != nil {
|
|
return nil, originalErr
|
|
}
|
|
wc := &wantCloser{ReadCloser: originalBodyCopy}
|
|
t.Cleanup(func() {
|
|
require.Equalf(t, wc.closeCount, 1, "did not close req body copy expected number of times at %s for req %#v; actual calls = %s", caller, req, wc.closeCalls)
|
|
})
|
|
return wc, nil
|
|
}
|
|
}
|
|
|
|
resp, err := rt.RoundTrip(req)
|
|
return false, resp, err
|
|
})
|
|
}
|
|
}
|
|
|
|
// wantCloseRespWrapper returns a transport.WrapperFunc that validates that the http.Response
|
|
// returned by the underlying http.RoundTripper is closed properly.
|
|
func wantCloseRespWrapper(t *testing.T) transport.WrapperFunc {
|
|
caller := getCaller()
|
|
return func(rt http.RoundTripper) http.RoundTripper {
|
|
return roundTripperFunc(func(req *http.Request) (bool, *http.Response, error) {
|
|
resp, err := rt.RoundTrip(req)
|
|
if err != nil {
|
|
// request failed, so there is no response body to watch for Close() calls on
|
|
return false, resp, err
|
|
}
|
|
wc := &wantCloser{ReadCloser: resp.Body}
|
|
t.Cleanup(func() {
|
|
require.False(t, wc.couldReadBytesJustBeforeClosing, "did not consume all response body bytes before closing %s", caller)
|
|
require.Equalf(t, wc.closeCount, 1, "did not close resp body expected number of times at %s for req %#v; actual calls = %s", caller, req, wc.closeCalls)
|
|
})
|
|
resp.Body = wc
|
|
return false, resp, err
|
|
})
|
|
}
|
|
}
|
|
|
|
type withFunc func(obj Object)
|
|
|
|
func with(obj Object, withFuncs ...withFunc) Object {
|
|
obj = obj.DeepCopyObject().(Object)
|
|
for _, withFunc := range withFuncs {
|
|
withFunc(obj)
|
|
}
|
|
return obj
|
|
}
|
|
|
|
func gvk(gvk schema.GroupVersionKind) withFunc {
|
|
return func(obj Object) {
|
|
obj.GetObjectKind().SetGroupVersionKind(gvk)
|
|
}
|
|
}
|
|
|
|
func annotations() withFunc {
|
|
return func(obj Object) {
|
|
obj.SetAnnotations(middlewareAnnotations)
|
|
}
|
|
}
|
|
|
|
func emptyAnnotations() withFunc {
|
|
return func(obj Object) {
|
|
obj.SetAnnotations(make(map[string]string))
|
|
}
|
|
}
|
|
|
|
func labels() withFunc {
|
|
return func(obj Object) {
|
|
obj.SetLabels(middlewareLabels)
|
|
}
|
|
}
|
|
|
|
func clusterName() withFunc {
|
|
return func(obj Object) {
|
|
obj.SetClusterName(someClusterName)
|
|
}
|
|
}
|
|
|
|
func createGetFederationDomainTest(t *testing.T, client *Client) {
|
|
t.Helper()
|
|
|
|
// create
|
|
federationDomain, err := client.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(goodFederationDomain.Namespace).
|
|
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodFederationDomain, federationDomain)
|
|
|
|
// read
|
|
federationDomain, err = client.PinnipedSupervisor.
|
|
ConfigV1alpha1().
|
|
FederationDomains(federationDomain.Namespace).
|
|
Get(context.Background(), federationDomain.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, goodFederationDomain, federationDomain)
|
|
}
|