7a812ac5ed
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>
312 lines
7.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|