ContainerImage.Pinniped/internal/controller/apicerts/certs_expirer_test.go
Monis Khan 0674215ef3
Switch to go.uber.org/zap for JSON formatted logging
Signed-off-by: Monis Khan <mok@vmware.com>
2022-05-24 11:17:42 -04:00

311 lines
8.2 KiB
Go

// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package apicerts
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"errors"
"io"
"strings"
"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/plog"
"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
plog.TestLogger(t, io.Discard),
)
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
wantLog string
wantError string
}{
{
name: "secret does not exist",
wantLog: `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"certs-expirer-controller","caller":"apicerts/certs_expirer.go:<line>$apicerts.(*certsExpirerController).Sync","message":"secret does not exist yet or was deleted","controller":"","namespace":"some-namespace","name":"some-resource-name","key":"some-awesome-key","renewBefore":"0s"}`,
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,
)
var log bytes.Buffer
c := NewCertsExpirerController(
namespace,
certsSecretResourceName,
kubeAPIClient,
kubeInformers.Core().V1().Secrets(),
controllerlib.WithInformer,
test.renewBefore,
fakeTestKey,
plog.TestLogger(t, &log),
)
// Must start informers before calling TestRunSynchronously().
kubeInformers.Start(ctx.Done())
controllerlib.TestRunSynchronously(t, c)
err := controllerlib.TestSync(t, c, controllerlib.Context{
Context: ctx,
})
if len(test.wantLog) > 0 {
require.Equal(t, test.wantLog, strings.TrimSpace(log.String()))
}
if test.wantError != "" {
require.EqualError(t, err, test.wantError)
return
}
require.NoError(t, err)
exActions := []kubetesting.Action{}
if test.wantDelete {
exActions = append(
exActions,
kubetesting.NewDeleteActionWithOptions(
schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "secrets",
},
namespace,
name,
testutil.NewPreconditions(testUID, testRV),
),
)
}
acActions := kubeAPIClient.Actions()
require.Equal(t, exActions, acActions)
})
}
}