cert-manager/test/integration/certificates/trigger_controller_test.go
Maël Valais 71e707387a trigger-controller: refactor test, inject gatherer and policychain
Injecting the whole Gatherer struct was not necessary for testing
since DataForCertificate is now fully unit-tested. With that, we
can mock the Gatherer.Evaluate function. Since there is no reason
to inject a full Gatherer object into the trigger controller, I chose
to inject a simple policies.Func. I named the function "shouldReissue"
since this is exactly what this function does.

I also refactored the test cases to use the same gen.Certificate
that we use in the rest of the codebase.

Signed-off-by: Maël Valais <mael@vls.dev>
2021-03-23 13:55:11 +01:00

291 lines
10 KiB
Go

/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package certificates
import (
"context"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
fakeclock "k8s.io/utils/clock/testing"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/controller/certificates/trigger"
"github.com/jetstack/cert-manager/pkg/controller/certificates/trigger/policies"
logf "github.com/jetstack/cert-manager/pkg/logs"
"github.com/jetstack/cert-manager/pkg/metrics"
"github.com/jetstack/cert-manager/pkg/util/pki"
utilpki "github.com/jetstack/cert-manager/pkg/util/pki"
"github.com/jetstack/cert-manager/test/integration/framework"
)
// TestTriggerController performs a basic test to ensure that the trigger
// controller works when instantiated.
// This is not an exhaustive set of test cases. It only ensures that an
// issuance is triggered when a new Certificate resource is created and
// no Secret exists.
func TestTriggerController(t *testing.T) {
config, stopFn := framework.RunControlPlane(t)
defer stopFn()
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*20)
defer cancel()
fakeClock := &fakeclock.FakeClock{}
// Build, instantiate and run the trigger controller.
kubeClient, factory, cmCl, cmFactory := framework.NewClients(t, config)
namespace := "testns"
// Create Namespace
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}
_, err := kubeClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// default certificate renewBefore period
defaultRenewBefore := time.Hour * 24
shouldReissue := policies.NewTriggerPolicyChain(fakeClock, defaultRenewBefore).Evaluate
ctrl, queue, mustSync := trigger.NewController(logf.Log, cmCl, factory, cmFactory, framework.NewEventRecorder(t), fakeClock, shouldReissue)
c := controllerpkg.NewController(
context.Background(),
"trigger_test",
metrics.New(logf.Log),
ctrl.ProcessItem,
mustSync,
nil,
queue,
)
stopController := framework.StartInformersAndController(t, factory, cmFactory, c)
defer stopController()
// Create a Certificate resource and wait for it to have the 'Issuing' condition.
cert, err := cmCl.CertmanagerV1().Certificates(namespace).Create(ctx, &cmapi.Certificate{
ObjectMeta: metav1.ObjectMeta{Name: "testcrt", Namespace: "testns"},
Spec: cmapi.CertificateSpec{
SecretName: "example",
CommonName: "example.com",
IssuerRef: cmmeta.ObjectReference{Name: "testissuer"}, // doesn't need to exist
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
err = wait.Poll(time.Millisecond*100, time.Second*5, func() (done bool, err error) {
c, err := cmCl.CertmanagerV1().Certificates(cert.Namespace).Get(ctx, cert.Name, metav1.GetOptions{})
if err != nil {
t.Logf("Failed to fetch Certificate resource, retrying: %v", err)
return false, nil
}
if !apiutil.CertificateHasCondition(c, cmapi.CertificateCondition{
Type: cmapi.CertificateConditionIssuing,
Status: cmmeta.ConditionTrue,
}) {
t.Logf("Certificate does not have expected condition, got=%#v", apiutil.GetCertificateCondition(c, cmapi.CertificateConditionIssuing))
return false, nil
}
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
func TestTriggerController_RenewNearExpiry(t *testing.T) {
config, stopFn := framework.RunControlPlane(t)
defer stopFn()
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*20)
defer cancel()
// default certificate renewBefore period
defaultRenewBefore := time.Hour * 24
fakeClock := &fakeclock.FakeClock{}
// Only use the 'current certificate nearing expiry' policy chain during the
// test as we want to test the very specific cases of triggering/not
// triggering depending on whether a renewal is required.
shoudReissue := policies.Chain{policies.CurrentCertificateNearingExpiry(fakeClock, defaultRenewBefore)}.Evaluate
// Build, instantiate and run the trigger controller.
kubeClient, factory, cmCl, cmFactory := framework.NewClients(t, config)
namespace := "testns"
secretName := "example"
certName := "testcrt"
now := fakeClock.Now()
notBefore := metav1.NewTime(now)
notAfter := metav1.NewTime(now.Add(time.Hour * 3))
renewBefore := &metav1.Duration{Duration: time.Hour}
// Create namespace
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}
_, err := kubeClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// Create Certificate template
cert := &cmapi.Certificate{
ObjectMeta: metav1.ObjectMeta{Name: certName, Namespace: namespace},
Spec: cmapi.CertificateSpec{
SecretName: secretName,
CommonName: "example.com",
RenewBefore: renewBefore,
IssuerRef: cmmeta.ObjectReference{Name: "testissuer"}, // doesn't need to exist
},
}
// Create a private key for X.509 cert
sk, err := utilpki.GenerateRSAPrivateKey(2048)
if err != nil {
t.Fatal(err)
}
skBytes := utilpki.EncodePKCS1PrivateKey(sk)
// Create an X.509 cert
x509CertBytes := selfSignCertificateWithNotBeforeAfter(t, skBytes, cert, notBefore.Time, notAfter.Time)
// Create a Secret with the X.509 cert
_, err = kubeClient.CoreV1().Secrets(namespace).Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Data: map[string][]byte{
corev1.TLSCertKey: x509CertBytes,
},
}, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// Start the trigger controller
ctrl, queue, mustSync := trigger.NewController(logf.Log, cmCl, factory, cmFactory, framework.NewEventRecorder(t), fakeClock, shoudReissue)
c := controllerpkg.NewController(
logf.NewContext(context.Background(), logf.Log, "trigger_controller_RenewNearExpiry"),
"trigger_test",
metrics.New(logf.Log),
ctrl.ProcessItem,
mustSync,
nil,
queue,
)
stopController := framework.StartInformersAndController(t, factory, cmFactory, c)
defer stopController()
// Create a Certificate
cert, err = cmCl.CertmanagerV1().Certificates(namespace).Create(ctx, cert, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// 1. Test that the Certificate's Issuing condition is not set to True when the
// X.509 cert is not approaching expiry.
// Wait for 2s, polling every 200ms to ensure that the controller does not set
// the condition.
t.Log("Ensuring Certificate does not have Issuing condition for 2s...")
ensureCertificateDoesNotHaveIssuingCondition(ctx, t, cmCl, namespace, certName)
// 2. Test that a Certificate does get the Issuing status condition set to
// True when the X.509 cert is nearing expiry.
t.Log("Advancing the clock forward to renewal time")
// Advance the clock to a millisecond after renewal time.
// fakeclock implementation uses .After when checking whether to trigger timers.
// renewalTime = notAfter - renewBefore
renewalTime := notAfter.Add(renewBefore.Duration * -1)
fakeClock.SetTime(renewalTime.Add(time.Millisecond * 2))
// Certificate's status.RenewalTime does not determine renewal, but we need to
// update some field to trigger a reconcile.
someRenewalTime := metav1.NewTime(now)
cert.Status.RenewalTime = &someRenewalTime
cert, err = cmCl.CertmanagerV1().Certificates(namespace).UpdateStatus(ctx, cert, metav1.UpdateOptions{})
err = wait.Poll(time.Millisecond*200, time.Second*2, func() (done bool, err error) {
c, err := cmCl.CertmanagerV1().Certificates(cert.Namespace).Get(ctx, cert.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if apiutil.CertificateHasCondition(c, cmapi.CertificateCondition{
Type: cmapi.CertificateConditionIssuing,
Status: cmmeta.ConditionTrue,
}) {
return true, nil
}
return false, nil
})
if err != nil {
t.Error("Failed waiting for Certificate to have Issuing condition")
}
}
func ensureCertificateDoesNotHaveIssuingCondition(ctx context.Context, t *testing.T, cmCl cmclient.Interface, namespace, name string) {
err := wait.Poll(time.Millisecond*200, time.Second*2, func() (done bool, err error) {
c, err := cmCl.CertmanagerV1().Certificates(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return false, err
}
if apiutil.CertificateHasCondition(c, cmapi.CertificateCondition{
Type: cmapi.CertificateConditionIssuing,
Status: cmmeta.ConditionTrue,
}) {
t.Logf("Certificate has unexpected 'Issuing' condition, got=%#v", apiutil.GetCertificateCondition(c, cmapi.CertificateConditionIssuing))
return true, nil
}
return false, nil
})
switch {
case err == nil:
t.Fatal("expected Certificate to not have the Issuing condition")
case err == wait.ErrWaitTimeout:
// this is the expected 'happy case'
default:
t.Fatal(err)
}
}
func selfSignCertificateWithNotBeforeAfter(t *testing.T, pkData []byte, spec *cmapi.Certificate, notBefore, notAfter time.Time) []byte {
pk, err := pki.DecodePrivateKeyBytes(pkData)
if err != nil {
t.Fatal(err)
}
template, err := pki.GenerateTemplate(spec)
if err != nil {
t.Fatal(err)
}
// override the NotAfter, NotBefore fields that by default are set based on time.Now
template.NotBefore = notBefore
template.NotAfter = notAfter
certData, _, err := pki.SignCertificate(template, template, pk.Public(), pk)
if err != nil {
t.Fatal(err)
}
return certData
}