cert-manager/pkg/controller/certificaterequests/sync_test.go
JoshVanL 8bbcf345c9 Changes CertificateRequest conditions to only Pending/Failed/Issued
Signed-off-by: JoshVanL <vleeuwenjoshua@gmail.com>
2019-07-16 15:31:12 +01:00

537 lines
18 KiB
Go

/*
Copyright 2019 The Jetstack cert-manager contributors.
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 certificaterequests
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"net"
"net/url"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
coretesting "k8s.io/client-go/testing"
clock "k8s.io/utils/clock/testing"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
"github.com/jetstack/cert-manager/pkg/issuer"
"github.com/jetstack/cert-manager/pkg/issuer/fake"
_ "github.com/jetstack/cert-manager/pkg/issuer/selfsigned"
"github.com/jetstack/cert-manager/pkg/util/pki"
"github.com/jetstack/cert-manager/test/unit/gen"
)
var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128)
func generateCSR(commonName string) ([]byte, error) {
csr := &x509.CertificateRequest{
Version: 3,
SignatureAlgorithm: x509.SHA256WithRSA,
PublicKeyAlgorithm: x509.RSA,
Subject: pkix.Name{
Organization: []string{"my-org"},
CommonName: commonName,
},
URIs: []*url.URL{
{
Scheme: "http",
Host: "example.com",
},
},
IPAddresses: []net.IP{
net.IPv4(8, 8, 8, 8),
},
}
sk, err := pki.GenerateRSAPrivateKey(2048)
if err != nil {
return nil, err
}
csrBytes, err := pki.EncodeCSR(csr, sk)
if err != nil {
return nil, err
}
csrPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST", Bytes: csrBytes,
})
return csrPEM, nil
}
func generatePrivateKey(t *testing.T) *rsa.PrivateKey {
pk, err := pki.GenerateRSAPrivateKey(2048)
if err != nil {
t.Errorf("failed to generate private key: %v", err)
t.FailNow()
}
return pk
}
func generateSelfSignedCert(t *testing.T, cr *cmapi.CertificateRequest, sn *big.Int, key crypto.Signer, notBefore, notAfter time.Time) []byte {
template, err := pki.GenerateTemplateFromCertificateRequest(cr)
if err != nil {
t.Errorf("failed to generate cert template from CSR: %v", err)
t.FailNow()
}
template.NotAfter = notAfter
template.NotBefore = notBefore
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if err != nil {
t.Errorf("error signing cert: %v", err)
t.FailNow()
}
pemByteBuffer := bytes.NewBuffer([]byte{})
err = pem.Encode(pemByteBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
t.Errorf("failed to encode cert: %v", err)
t.FailNow()
}
return pemByteBuffer.Bytes()
}
func TestSync(t *testing.T) {
nowTime := time.Now()
nowMetaTime := metav1.NewTime(nowTime)
fixedClock := clock.NewFakeClock(nowTime)
csr, err := generateCSR("csr")
if err != nil {
t.Errorf("failed to generate CSR for testing: %s", err)
t.FailNow()
}
pk := generatePrivateKey(t)
exampleCR := gen.CertificateRequest("test",
gen.SetCertificateRequestIsCA(false),
gen.SetCertificateRequestIssuer(cmapi.ObjectReference{Name: "test"}),
gen.SetCertificateRequestCSR(csr),
gen.SetCertificateRequestIssuer(cmapi.ObjectReference{
Kind: "Issuer",
Name: "fake-issuer",
}),
)
exampleCRIssuePendingCondition := gen.CertificateRequestFrom(exampleCR,
gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
Type: cmapi.CertificateRequestConditionReady,
Status: cmapi.ConditionFalse,
Reason: "Pending",
Message: "Certificate issuance pending",
LastTransitionTime: &nowMetaTime,
}),
)
exampleCRIssuerNotFoundPendingCondition := gen.CertificateRequestFrom(exampleCR,
gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
Type: cmapi.CertificateRequestConditionReady,
Status: cmapi.ConditionFalse,
Reason: "Pending",
Message: "Referenced Issuer not found",
LastTransitionTime: &nowMetaTime,
}),
)
exampleFailedCR := gen.CertificateRequestFrom(exampleCR,
gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
Type: cmapi.CertificateRequestConditionReady,
Status: cmapi.ConditionFalse,
Reason: errorFailed,
LastTransitionTime: &nowMetaTime,
}),
)
certPEM := generateSelfSignedCert(t, exampleCR, nil, pk, nowTime, nowTime.Add(time.Hour*12))
certPEMExpired := generateSelfSignedCert(t, exampleCR, nil, pk, nowTime.Add(-time.Hour*13), nowTime.Add(-time.Hour*12))
exampleSignedCR := exampleCR.DeepCopy()
exampleSignedCR.Status.Certificate = certPEM
exampleSignedExpiredCR := exampleCR.DeepCopy()
exampleSignedExpiredCR.Status.Certificate = certPEMExpired
exampleCRReadyCondition := gen.CertificateRequestFrom(exampleSignedCR,
gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
Type: cmapi.CertificateRequestConditionReady,
Status: cmapi.ConditionTrue,
Reason: "Ready",
Message: "Certificate has been issued successfully",
LastTransitionTime: &nowMetaTime,
}),
)
exampleCRExpiredReadyCondition := exampleSignedExpiredCR
exampleCRExpiredReadyCondition.Status.Conditions = exampleCRReadyCondition.Status.Conditions
exampleGarbageCertCR := exampleSignedCR.DeepCopy()
exampleGarbageCertCR.Status.Certificate = []byte("not a certificate")
exampleCRGarbageCondition := gen.CertificateRequestFrom(exampleGarbageCertCR,
gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
Type: cmapi.CertificateRequestConditionReady,
Status: cmapi.ConditionFalse,
Reason: "Failed",
Message: "Failed to decode certificate PEM",
LastTransitionTime: &nowMetaTime,
}),
)
exampleEmptyCSRCR := exampleCR.DeepCopy()
exampleEmptyCSRCR.Spec.CSRPEM = make([]byte, 0)
exampleFailedValidationCR := gen.CertificateRequestFrom(exampleEmptyCSRCR,
gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
Type: cmapi.CertificateRequestConditionReady,
Status: cmapi.ConditionFalse,
Reason: "Failed",
Message: "Validation failed: spec.csr: Required value: must be specified",
LastTransitionTime: &nowMetaTime,
}),
)
exampleCRWrongIssuerRefGroup := exampleCR.DeepCopy()
exampleCRWrongIssuerRefGroup.Spec.IssuerRef.Group = "notcertmanager.k8s.io"
exampleCRCorrentIssuerRefGroup := exampleCRWrongIssuerRefGroup.DeepCopy()
exampleCRCorrentIssuerRefGroup.Spec.IssuerRef.Group = "certmanager.k8s.io"
exampleCRReadyConditionWithGroupRef := exampleCRReadyCondition.DeepCopy()
exampleCRReadyConditionWithGroupRef.Spec.IssuerRef.Group = "certmanager.k8s.io"
tests := map[string]controllerFixture{
"should update certificate request with CertPending if issuer does not return a response": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
// By not returning a response, we trigger a 'no-op' action which
// causes the certificate request controller to update the status of
// the CertificateRequest with !Ready - CertPending.
return nil, nil
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
gen.DefaultTestNamespace,
exampleCRIssuePendingCondition,
)),
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"should update the status with a freshly signed certificate only when one doesn't exist and group ref=''": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return &issuer.IssueResponse{
Certificate: certPEM,
}, nil
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
gen.DefaultTestNamespace,
exampleCRReadyCondition,
)),
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"should update the status with a freshly signed certificate only when one doesn't exist and issuer group ref='certmanager.k8s.io'": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleCRCorrentIssuerRefGroup,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return &issuer.IssueResponse{
Certificate: certPEM,
}, nil
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
gen.DefaultTestNamespace,
exampleCRReadyConditionWithGroupRef,
)),
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"should exit sync nil if issuerRef group does not match certmanager.k8s.io": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleCRWrongIssuerRefGroup,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{}, // no update
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"should not update certificate request if certificate exists, even if out of date": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleSignedExpiredCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{}, // no update
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"fail if bytes contains no certificate but len > 0": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleGarbageCertCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
gen.DefaultTestNamespace,
exampleCRGarbageCondition,
)),
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"return nil if generic issuer doesn't exist, will sync when on ready": {
Issuer: nil,
CertificateRequest: *exampleCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
gen.DefaultTestNamespace,
exampleCRIssuerNotFoundPendingCondition,
)),
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"exit nil if we cannot determine the issuer type (probably not meant for us)": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
// no issuer set
),
CertificateRequest: *exampleCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
gen.DefaultTestNamespace,
exampleCRIssuerNotFoundPendingCondition,
)),
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"exit nil if the issuer type is not meant for us": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerCA(cmapi.CAIssuer{}),
),
CertificateRequest: *exampleSignedCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"exit if we fail validation during a sync": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleEmptyCSRCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
gen.DefaultTestNamespace,
exampleFailedValidationCR,
)),
},
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
"should exit sync nil if condition is failed": {
Issuer: gen.Issuer("test",
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmapi.ConditionTrue,
}),
gen.SetIssuerSelfSigned(cmapi.SelfSignedIssuer{}),
),
CertificateRequest: *exampleFailedCR,
IssuerImpl: &fake.Issuer{
FakeSign: func(context.Context, *cmapi.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("unexpected sign call")
},
},
Builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{gen.CertificateRequest("test")},
ExpectedActions: []testpkg.Action{}, // no update
},
CheckFn: func(t *testing.T, s *controllerFixture, args ...interface{}) {
},
Err: false,
},
}
for n, test := range tests {
t.Run(n, func(t *testing.T) {
if test.Builder == nil {
test.Builder = &testpkg.Builder{}
}
test.Clock = fixedClock
test.Setup(t)
crCopy := test.CertificateRequest.DeepCopy()
err := test.controller.Sync(test.Ctx, crCopy)
if err != nil && !test.Err {
t.Errorf("Expected function to not error, but got: %v", err)
}
if err == nil && test.Err {
t.Errorf("Expected function to get an error, but got: %v", err)
}
test.Finish(t, crCopy, err)
})
}
}