ContainerImage.Pinniped/internal/controller/apicerts/certs_expirer_test.go
Monis Khan 7a812ac5ed
impersonatorconfig: only unload dynamiccert when proxy is disabled
In the upstream dynamiccertificates package, we rely on two pieces
of code:

1. DynamicServingCertificateController.newTLSContent which calls
   - clientCA.CurrentCABundleContent
   - servingCert.CurrentCertKeyContent
2. unionCAContent.VerifyOptions which calls
   - unionCAContent.CurrentCABundleContent

This results in calls to our tlsServingCertDynamicCertProvider and
impersonationSigningCertProvider.  If we Unset these providers, we
subtly break these consumers.  At best this results in test slowness
and flakes while we wait for reconcile loops to converge.  At worst,
it results in actual errors during runtime.  For example, we
previously would Unset the impersonationSigningCertProvider on any
sync loop error (even a transient one caused by a network blip or
a conflict between writes from different replicas of the concierge).
This would cause us to transiently fail to issue new certificates
from the token credential require API.  It would also cause us to
transiently fail to authenticate previously issued client certs
(which results in occasional Unauthorized errors in CI).

Signed-off-by: Monis Khan <mok@vmware.com>
2021-08-16 16:07:46 -04:00

312 lines
7.9 KiB
Go

// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package apicerts
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
kubeinformers "k8s.io/client-go/informers"
kubernetesfake "k8s.io/client-go/kubernetes/fake"
kubetesting "k8s.io/client-go/testing"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/testutil"
)
func TestExpirerControllerFilters(t *testing.T) {
t.Parallel()
const certsSecretResourceName = "some-resource-name"
tests := []struct {
name string
namespace string
secret corev1.Secret
want bool
}{
{
name: "good name, good namespace",
namespace: "good-namespace",
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: certsSecretResourceName,
Namespace: "good-namespace",
},
},
want: true,
},
{
name: "bad name, good namespace",
namespace: "good-namespacee",
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-name",
Namespace: "good-namespace",
},
},
want: false,
},
{
name: "good name, bad namespace",
namespace: "good-namespacee",
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: certsSecretResourceName,
Namespace: "bad-namespace",
},
},
want: false,
},
{
name: "bad name, bad namespace",
namespace: "good-namespacee",
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-name",
Namespace: "bad-namespace",
},
},
want: false,
},
}
for _, test := range tests {
test := test
t.Run(test.name+"-"+test.namespace, func(t *testing.T) {
t.Parallel()
secretsInformer := kubeinformers.NewSharedInformerFactory(
kubernetesfake.NewSimpleClientset(),
0,
).Core().V1().Secrets()
withInformer := testutil.NewObservableWithInformerOption()
_ = NewCertsExpirerController(
test.namespace,
certsSecretResourceName,
nil, // k8sClient, not needed
secretsInformer,
withInformer.WithInformer,
0, // renewBefore, not needed
"", // not needed
)
unrelated := corev1.Secret{}
filter := withInformer.GetFilterForInformer(secretsInformer)
require.Equal(t, test.want, filter.Add(&test.secret))
require.Equal(t, test.want, filter.Update(&unrelated, &test.secret))
require.Equal(t, test.want, filter.Update(&test.secret, &unrelated))
require.Equal(t, test.want, filter.Delete(&test.secret))
})
}
}
func TestExpirerControllerSync(t *testing.T) {
t.Parallel()
const certsSecretResourceName = "some-resource-name"
const fakeTestKey = "some-awesome-key"
tests := []struct {
name string
renewBefore time.Duration
fillSecretData func(*testing.T, map[string][]byte)
configKubeAPIClient func(*kubernetesfake.Clientset)
wantDelete bool
wantError string
}{
{
name: "secret does not exist",
wantDelete: false,
},
{
name: "secret missing key",
fillSecretData: func(t *testing.T, m map[string][]byte) {},
wantDelete: false,
wantError: `failed to get cert bounds for secret "some-resource-name" with key "some-awesome-key": failed to find certificate`,
},
{
name: "lifetime below threshold",
renewBefore: 7 * time.Hour,
fillSecretData: func(t *testing.T, m map[string][]byte) {
certPEM, _, err := testutil.CreateCertificate(
time.Now().Add(-5*time.Hour),
time.Now().Add(5*time.Hour),
)
require.NoError(t, err)
m[fakeTestKey] = certPEM
},
wantDelete: false,
},
{
name: "lifetime above threshold",
renewBefore: 3 * time.Hour,
fillSecretData: func(t *testing.T, m map[string][]byte) {
certPEM, _, err := testutil.CreateCertificate(
time.Now().Add(-5*time.Hour),
time.Now().Add(5*time.Hour),
)
require.NoError(t, err)
m[fakeTestKey] = certPEM
},
wantDelete: true,
},
{
name: "cert expired",
renewBefore: 3 * time.Hour,
fillSecretData: func(t *testing.T, m map[string][]byte) {
certPEM, _, err := testutil.CreateCertificate(
time.Now().Add(-2*time.Hour),
time.Now().Add(-1*time.Hour),
)
require.NoError(t, err)
m[fakeTestKey] = certPEM
},
wantDelete: true,
},
{
name: "delete failure",
renewBefore: 3 * time.Hour,
fillSecretData: func(t *testing.T, m map[string][]byte) {
certPEM, _, err := testutil.CreateCertificate(
time.Now().Add(-5*time.Hour),
time.Now().Add(5*time.Hour),
)
require.NoError(t, err)
m[fakeTestKey] = certPEM
},
configKubeAPIClient: func(c *kubernetesfake.Clientset) {
c.PrependReactor("delete", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("delete failed: some delete error")
})
},
wantError: "delete failed: some delete error",
},
{
name: "parse cert failure",
fillSecretData: func(t *testing.T, m map[string][]byte) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
m[fakeTestKey], err = x509.MarshalPKCS8PrivateKey(privateKey)
require.NoError(t, err)
},
wantDelete: false,
wantError: `failed to get cert bounds for secret "some-resource-name" with key "some-awesome-key": failed to decode certificate PEM`,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
kubeAPIClient := kubernetesfake.NewSimpleClientset()
if test.configKubeAPIClient != nil {
test.configKubeAPIClient(kubeAPIClient)
}
testRV := "rv_001"
testUID := types.UID("uid_002")
kubeInformerClient := kubernetesfake.NewSimpleClientset()
name := certsSecretResourceName
namespace := "some-namespace"
if test.fillSecretData != nil {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
ResourceVersion: testRV,
UID: testUID,
},
Data: map[string][]byte{},
}
test.fillSecretData(t, secret.Data)
require.NoError(t, kubeAPIClient.Tracker().Add(secret))
require.NoError(t, kubeInformerClient.Tracker().Add(secret))
}
kubeInformers := kubeinformers.NewSharedInformerFactory(
kubeInformerClient,
0,
)
opts := &[]metav1.DeleteOptions{}
trackDeleteClient := testutil.NewDeleteOptionsRecorder(kubeAPIClient, opts)
c := NewCertsExpirerController(
namespace,
certsSecretResourceName,
trackDeleteClient,
kubeInformers.Core().V1().Secrets(),
controllerlib.WithInformer,
test.renewBefore,
fakeTestKey,
)
// Must start informers before calling TestRunSynchronously().
kubeInformers.Start(ctx.Done())
controllerlib.TestRunSynchronously(t, c)
err := controllerlib.TestSync(t, c, controllerlib.Context{
Context: ctx,
})
if test.wantError != "" {
require.EqualError(t, err, test.wantError)
return
}
require.NoError(t, err)
exActions := []kubetesting.Action{}
if test.wantDelete {
exActions = append(
exActions,
kubetesting.NewDeleteAction(
schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "secrets",
},
namespace,
name,
),
)
}
acActions := kubeAPIClient.Actions()
require.Equal(t, exActions, acActions)
if test.wantDelete {
require.Len(t, *opts, 1)
require.Equal(t, metav1.DeleteOptions{
Preconditions: &metav1.Preconditions{
UID: &testUID,
ResourceVersion: &testRV,
},
}, (*opts)[0])
} else {
require.Len(t, *opts, 0)
}
})
}
}