
This change updates the apicerts controllers to return an error when they cannot successfully complete their Sync func. i.e. `return nil` is reserved for cases where the controller has fully completed its job with no errors. This makes it clear when a controller has wedged - i.e. it is waiting on some other controller or process to perform some action before it can complete. The controller lib's queue will exponentially back off and thus there is no need to be concerned with returning an error indefinitely or infinite log spam. Even when the kubelet throws away container logs, it will be clear what controllers are wedged based on the last hour or so of logs. Signed-off-by: Monis Khan <mok@vmware.com>
294 lines
7.7 KiB
Go
294 lines
7.7 KiB
Go
// Copyright 2020 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"
|
|
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
|
|
)
|
|
|
|
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"
|
|
|
|
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,
|
|
wantError: "certsExpirerController missing pre-requirements, secret some-namespace/some-resource-name does not exist: synthetic requeue request",
|
|
},
|
|
{
|
|
name: "secret missing key",
|
|
fillSecretData: func(t *testing.T, m map[string][]byte) {},
|
|
wantDelete: false,
|
|
wantError: "certsExpirerController Sync found that the secret is malformed: 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)
|
|
|
|
// See certs_manager.go for this constant.
|
|
m["tlsCertificateChain"] = 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)
|
|
|
|
// See certs_manager.go for this constant.
|
|
m["tlsCertificateChain"] = 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)
|
|
|
|
// See certs_manager.go for this constant.
|
|
m["tlsCertificateChain"] = 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)
|
|
|
|
// See certs_manager.go for this constant.
|
|
m["tlsCertificateChain"] = 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)
|
|
|
|
// See certs_manager.go for this constant.
|
|
m["tlsCertificateChain"], err = x509.MarshalPKCS8PrivateKey(privateKey)
|
|
require.NoError(t, err)
|
|
},
|
|
wantDelete: false,
|
|
wantError: "certsExpirerController Sync found that the secret is malformed: failed to decode certificate PEM",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
|
defer cancel()
|
|
|
|
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
|
if test.configKubeAPIClient != nil {
|
|
test.configKubeAPIClient(kubeAPIClient)
|
|
}
|
|
|
|
kubeInformerClient := kubernetesfake.NewSimpleClientset()
|
|
name := certsSecretResourceName
|
|
namespace := "some-namespace"
|
|
if test.fillSecretData != nil {
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
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,
|
|
)
|
|
|
|
c := NewCertsExpirerController(
|
|
namespace,
|
|
certsSecretResourceName,
|
|
kubeAPIClient,
|
|
kubeInformers.Core().V1().Secrets(),
|
|
controllerlib.WithInformer,
|
|
test.renewBefore,
|
|
)
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
}
|