ContainerImage.Pinniped/test/integration/kubeclient_test.go
Andrew Keesler 8a916ce8ae
test/integration: add test helper to avoid race conditions
We were seeing a race in this test code since the require.NoError() and
require.Eventually() would write to the same testing.T state on separate
goroutines. Hopefully this helper function should cover the cases when we want
to require.NoError() inside a require.Eventually() without causing a race.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
Co-authored-by: Margo Crawford <margaretc@vmware.com>
Co-authored-by: Monis Khan <i@monis.app>
2021-01-14 10:19:35 -05:00

273 lines
9.3 KiB
Go

// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
conciergeconfigv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/config/v1alpha1"
supervisorconfigv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1"
"go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/ownerref"
"go.pinniped.dev/test/library"
)
func TestKubeClientOwnerRef(t *testing.T) {
env := library.IntegrationEnv(t)
regularClient := library.NewClientset(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
namespaces := regularClient.CoreV1().Namespaces()
namespace, err := namespaces.Create(
ctx,
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: "test-owner-ref-"}},
metav1.CreateOptions{},
)
require.NoError(t, err)
defer func() {
if t.Failed() {
return
}
err := namespaces.Delete(ctx, namespace.Name, metav1.DeleteOptions{})
require.NoError(t, err)
}()
// create something that we can point to
parentSecret, err := regularClient.CoreV1().Secrets(namespace.Name).Create(
ctx,
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "parent-",
OwnerReferences: nil, // no owner refs set
},
Data: map[string][]byte{"A": []byte("B")},
},
metav1.CreateOptions{},
)
require.NoError(t, err)
require.Len(t, parentSecret.OwnerReferences, 0)
// create a client that should set an owner ref back to parent on create
ref := metav1.OwnerReference{
APIVersion: "v1",
Kind: "Secret",
Name: parentSecret.Name,
UID: parentSecret.UID,
}
ownerRefClient, err := kubeclient.New(
kubeclient.WithMiddleware(ownerref.New(ref)),
kubeclient.WithConfig(library.NewClientConfig(t)),
)
require.NoError(t, err)
ownerRefSecrets := ownerRefClient.Kubernetes.CoreV1().Secrets(namespace.Name)
// we expect this secret to have the owner ref set even though we did not set it explicitly
childSecret, err := ownerRefSecrets.Create(
ctx,
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "child-",
OwnerReferences: nil, // no owner refs set
},
Data: map[string][]byte{"C": []byte("D")},
},
metav1.CreateOptions{},
)
require.NoError(t, err)
hasOwnerRef(t, childSecret, ref)
preexistingRef := *ref.DeepCopy()
preexistingRef.Name = "different"
preexistingRef.UID = "different"
// we expect this secret to keep the owner ref that is was created with
otherSecret, err := ownerRefSecrets.Create(
ctx,
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "child-",
OwnerReferences: []metav1.OwnerReference{preexistingRef}, // owner ref set explicitly
},
Data: map[string][]byte{"C": []byte("D")},
},
metav1.CreateOptions{},
)
require.NoError(t, err)
hasOwnerRef(t, otherSecret, preexistingRef)
require.NotEqual(t, ref, preexistingRef)
// the secret has no owner so it should be immediately deleted
isEventuallyDeleted(t, func() error {
_, err := ownerRefSecrets.Get(ctx, otherSecret.Name, metav1.GetOptions{})
return err
})
// we expect no owner ref to be set on update
parentSecretUpdate := parentSecret.DeepCopy()
parentSecretUpdate.Data = map[string][]byte{"E": []byte("F ")}
updatedParentSecret, err := ownerRefSecrets.Update(ctx, parentSecretUpdate, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, parentSecret.UID, updatedParentSecret.UID)
require.NotEqual(t, parentSecret.ResourceVersion, updatedParentSecret.ResourceVersion)
require.Len(t, updatedParentSecret.OwnerReferences, 0)
// delete the parent object
err = ownerRefSecrets.Delete(ctx, parentSecret.Name, metav1.DeleteOptions{})
require.NoError(t, err)
// the child object should be cleaned up on its own
isEventuallyDeleted(t, func() error {
_, err := ownerRefSecrets.Get(ctx, childSecret.Name, metav1.GetOptions{})
return err
})
// TODO: update middleware code to not set owner references on cluster-scoped objects.
//
// The Kube 1.20 garbage collector asserts some new behavior in regards to invalid owner
// references (i.e., when you have a namespace-scoped owner references for a cluster-scoped
// dependent, the cluster-scoped dependent is not removed). We also found a bug in the 1.20
// garbage collector where namespace-scoped dependents are not garbage collected if their owner
// had been used as an invalid owner reference before - this bug causes our test to fallover
// because we are setting a namespace-scoped owner ref on this APIService.
//
// We believe that the best way to get around this problem is to update our kubeclient code to
// never set owner references on cluster-scoped objects. After we do that, we will uncomment this
// part of the test.
if false {
// sanity check API service client
apiService, err := ownerRefClient.Aggregation.ApiregistrationV1().APIServices().Create(
ctx,
&apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{
Name: "v1.pandas.dev",
OwnerReferences: nil, // no owner refs set
},
Spec: apiregistrationv1.APIServiceSpec{
Version: "v1",
Group: "pandas.dev",
GroupPriorityMinimum: 10_000,
VersionPriority: 500,
},
},
metav1.CreateOptions{},
)
require.NoError(t, err)
hasOwnerRef(t, apiService, ref)
// this owner ref is invalid for an API service so it should be immediately deleted
isEventuallyDeleted(t, func() error {
_, err := ownerRefClient.Aggregation.ApiregistrationV1().APIServices().Get(ctx, apiService.Name, metav1.GetOptions{})
return err
})
}
// sanity check concierge client
credentialIssuer, err := ownerRefClient.PinnipedConcierge.ConfigV1alpha1().CredentialIssuers(namespace.Name).Create(
ctx,
&conciergeconfigv1alpha1.CredentialIssuer{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "owner-ref-test-",
OwnerReferences: nil, // no owner refs set
},
Status: conciergeconfigv1alpha1.CredentialIssuerStatus{
Strategies: []conciergeconfigv1alpha1.CredentialIssuerStrategy{},
},
},
metav1.CreateOptions{},
)
require.NoError(t, err)
hasOwnerRef(t, credentialIssuer, ref)
// this owner has already been deleted so the cred issuer should be immediately deleted
isEventuallyDeleted(t, func() error {
_, err := ownerRefClient.PinnipedConcierge.ConfigV1alpha1().CredentialIssuers(namespace.Name).Get(ctx, credentialIssuer.Name, metav1.GetOptions{})
return err
})
// sanity check supervisor client
federationDomain, err := ownerRefClient.PinnipedSupervisor.ConfigV1alpha1().FederationDomains(namespace.Name).Create(
ctx,
&supervisorconfigv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "owner-ref-test-",
OwnerReferences: nil, // no owner refs set
},
Spec: supervisorconfigv1alpha1.FederationDomainSpec{
Issuer: "https://pandas.dev",
},
},
metav1.CreateOptions{},
)
require.NoError(t, err)
hasOwnerRef(t, federationDomain, ref)
// this owner has already been deleted so the federation domain should be immediately deleted
isEventuallyDeleted(t, func() error {
_, err := ownerRefClient.PinnipedSupervisor.ConfigV1alpha1().FederationDomains(namespace.Name).Get(ctx, federationDomain.Name, metav1.GetOptions{})
return err
})
// check some well-known, always created secrets to make sure they have an owner ref back to their deployment
dref := metav1.OwnerReference{}
dref.APIVersion, dref.Kind = appsv1.SchemeGroupVersion.WithKind("Deployment").ToAPIVersionAndKind()
supervisorDeployment, err := ownerRefClient.Kubernetes.AppsV1().Deployments(env.SupervisorNamespace).Get(ctx, env.SupervisorAppName, metav1.GetOptions{})
require.NoError(t, err)
supervisorKey, err := ownerRefClient.Kubernetes.CoreV1().Secrets(env.SupervisorNamespace).Get(ctx, env.SupervisorAppName+"-key", metav1.GetOptions{})
require.NoError(t, err)
supervisorDref := *dref.DeepCopy()
supervisorDref.Name = env.SupervisorAppName
supervisorDref.UID = supervisorDeployment.UID
hasOwnerRef(t, supervisorKey, supervisorDref)
conciergeDeployment, err := ownerRefClient.Kubernetes.AppsV1().Deployments(env.ConciergeNamespace).Get(ctx, env.ConciergeAppName, metav1.GetOptions{})
require.NoError(t, err)
conciergeCert, err := ownerRefClient.Kubernetes.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, env.ConciergeAppName+"-api-tls-serving-certificate", metav1.GetOptions{})
require.NoError(t, err)
conciergeDref := *dref.DeepCopy()
conciergeDref.Name = env.ConciergeAppName
conciergeDref.UID = conciergeDeployment.UID
hasOwnerRef(t, conciergeCert, conciergeDref)
}
func hasOwnerRef(t *testing.T, obj metav1.Object, ref metav1.OwnerReference) {
t.Helper()
ownerReferences := obj.GetOwnerReferences()
require.Len(t, ownerReferences, 1)
require.Equal(t, ref, ownerReferences[0])
}
func isEventuallyDeleted(t *testing.T, f func() error) {
t.Helper()
library.RequireEventuallyWithoutError(t, func() (bool, error) {
err := f()
switch {
case err == nil:
return false, nil
case errors.IsNotFound(err):
return true, nil
default:
return false, err
}
}, time.Minute, time.Second)
}