1071 lines
36 KiB
Go
1071 lines
36 KiB
Go
/*
|
|
Copyright 2021 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 acme
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
authzv1 "k8s.io/api/authorization/v1"
|
|
certificatesv1 "k8s.io/api/certificates/v1"
|
|
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/client-go/kubernetes"
|
|
coretesting "k8s.io/client-go/testing"
|
|
fakeclock "k8s.io/utils/clock/testing"
|
|
|
|
cmacme "github.com/cert-manager/cert-manager/pkg/apis/acme/v1"
|
|
"github.com/cert-manager/cert-manager/pkg/apis/certmanager"
|
|
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
|
cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
|
|
cmclient "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned"
|
|
"github.com/cert-manager/cert-manager/pkg/controller/certificatesigningrequests/util"
|
|
testpkg "github.com/cert-manager/cert-manager/pkg/controller/test"
|
|
"github.com/cert-manager/cert-manager/pkg/util/pki"
|
|
"github.com/cert-manager/cert-manager/test/unit/gen"
|
|
)
|
|
|
|
var (
|
|
fixedClockStart = time.Now()
|
|
fixedClock = fakeclock.NewFakeClock(fixedClockStart)
|
|
|
|
certificatesigningrequestGVK = schema.GroupVersionKind{Group: "certificates.k8s.io", Version: "v1", Kind: "CertificateSigningRequest"}
|
|
)
|
|
|
|
func Test_controllerBuilder(t *testing.T) {
|
|
baseCSR := gen.CertificateSigningRequest("test-csr",
|
|
gen.SetCertificateSigningRequestCertificate([]byte("csr")),
|
|
)
|
|
|
|
baseOrder := gen.Order("test-order",
|
|
gen.SetOrderNamespace("test-namespace"),
|
|
)
|
|
|
|
tests := map[string]struct {
|
|
existingCSR runtime.Object
|
|
existingCMObjects []runtime.Object
|
|
givenCall func(*testing.T, cmclient.Interface, kubernetes.Interface)
|
|
expectRequeueKey string
|
|
}{
|
|
"if no request then no request should sync": {
|
|
existingCSR: nil,
|
|
existingCMObjects: []runtime.Object{baseOrder},
|
|
givenCall: func(t *testing.T, _ cmclient.Interface, _ kubernetes.Interface) {},
|
|
expectRequeueKey: "",
|
|
},
|
|
"if no changes to request or order, then no request should sync": {
|
|
existingCSR: baseCSR,
|
|
existingCMObjects: []runtime.Object{baseOrder},
|
|
givenCall: func(t *testing.T, _ cmclient.Interface, _ kubernetes.Interface) {},
|
|
expectRequeueKey: "",
|
|
},
|
|
"request should be synced if an owned order is updated": {
|
|
existingCSR: baseCSR,
|
|
existingCMObjects: []runtime.Object{
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderOwnerReference(*metav1.NewControllerRef(baseCSR, certificatesigningrequestGVK)),
|
|
),
|
|
},
|
|
givenCall: func(t *testing.T, cmclient cmclient.Interface, _ kubernetes.Interface) {
|
|
order := gen.OrderFrom(baseOrder,
|
|
gen.SetOrderOwnerReference(*metav1.NewControllerRef(baseCSR, certificatesigningrequestGVK)),
|
|
gen.SetOrderURL("update"),
|
|
)
|
|
_, err := cmclient.AcmeV1().Orders("test-namespace").Update(context.TODO(), order, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
expectRequeueKey: "test-csr",
|
|
},
|
|
"request should not be synced if updated order is not owned": {
|
|
existingCSR: baseCSR,
|
|
existingCMObjects: []runtime.Object{
|
|
gen.OrderFrom(baseOrder),
|
|
},
|
|
givenCall: func(t *testing.T, cmclient cmclient.Interface, _ kubernetes.Interface) {
|
|
order := gen.OrderFrom(baseOrder,
|
|
gen.SetOrderURL("update"),
|
|
)
|
|
_, err := cmclient.AcmeV1().Orders("test-namespace").Update(context.TODO(), order, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
expectRequeueKey: "",
|
|
},
|
|
"request should be synced if request is updated": {
|
|
existingCSR: baseCSR,
|
|
existingCMObjects: []runtime.Object{baseOrder},
|
|
givenCall: func(t *testing.T, _ cmclient.Interface, kubeclient kubernetes.Interface) {
|
|
csr := gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestCertificate([]byte("update")),
|
|
)
|
|
_, err := kubeclient.CertificatesV1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
},
|
|
expectRequeueKey: "test-csr",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
b := &testpkg.Builder{T: t, CertManagerObjects: test.existingCMObjects}
|
|
if test.existingCSR != nil {
|
|
b.KubeObjects = append(b.KubeObjects, test.existingCSR)
|
|
}
|
|
b.Init()
|
|
|
|
queue, hasSynced, err := controllerBuilder().Register(b.Context)
|
|
require.NoError(t, err)
|
|
|
|
b.Start()
|
|
defer b.Stop()
|
|
|
|
for _, hs := range hasSynced {
|
|
require.True(t, hs())
|
|
}
|
|
|
|
// Remove all objects from the queue before continuing.
|
|
for queue.Len() != 0 {
|
|
o, _ := queue.Get()
|
|
queue.Done(o)
|
|
}
|
|
|
|
test.givenCall(t, b.CMClient, b.Client)
|
|
|
|
// We have no way of knowing when the informers will be done adding
|
|
// items to the queue due to the "shared informer" architecture:
|
|
// Start(stop) does not allow you to wait for the informers to be
|
|
// done. To work around that, we do a second queue.Get and expect it
|
|
// to be nil.
|
|
time.AfterFunc(50*time.Millisecond, queue.ShutDown)
|
|
|
|
var gotKeys []string
|
|
for {
|
|
// Get blocks until either (1) a key is returned, or (2) the
|
|
// queue is shut down.
|
|
gotKey, done := queue.Get()
|
|
if done {
|
|
break
|
|
}
|
|
gotKeys = append(gotKeys, gotKey.(string))
|
|
}
|
|
assert.Equal(t, 0, queue.Len(), "queue should be empty")
|
|
|
|
// We only expect 0 or 1 keys received in the queue.
|
|
if test.expectRequeueKey != "" {
|
|
assert.Equal(t, []string{test.expectRequeueKey}, gotKeys)
|
|
} else {
|
|
assert.Nil(t, gotKeys)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ProcessItem(t *testing.T) {
|
|
metaFixedClockStart := metav1.NewTime(fixedClockStart)
|
|
util.Clock = fixedClock
|
|
|
|
baseIssuer := gen.Issuer("test-issuer",
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
gen.AddIssuerCondition(cmapi.IssuerCondition{
|
|
Type: cmapi.IssuerConditionReady,
|
|
Status: cmmeta.ConditionTrue,
|
|
}),
|
|
)
|
|
|
|
csrPEM, sk, err := gen.CSR(x509.ECDSA,
|
|
gen.SetCSRCommonName("example.com"),
|
|
gen.SetCSRDNSNames("example.com"),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req, err := pki.DecodeX509CertificateRequestBytes(csrPEM)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
csrPEMExampleNotPresent, skExampleNotPresent, err := gen.CSR(x509.ECDSA,
|
|
gen.SetCSRCommonName("example.com"),
|
|
gen.SetCSRDNSNames("foo.com"),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
baseCSR := gen.CertificateSigningRequest("test-csr",
|
|
gen.SetCertificateSigningRequestRequest(csrPEM),
|
|
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/default-unit-test-ns.test-issuer"),
|
|
gen.SetCertificateSigningRequestDuration("1440h"),
|
|
gen.SetCertificateSigningRequestUsername("user-1"),
|
|
gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
|
|
gen.SetCertificateSigningRequestUID("uid-1"),
|
|
gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
}),
|
|
)
|
|
|
|
tmpl, err := pki.CertificateTemplateFromCertificateSigningRequest(baseCSR)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
certPEM, _, err := pki.SignCertificate(tmpl, tmpl, sk.Public(), sk)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tmpl, err = pki.CertificateTemplateFromCertificateSigningRequest(gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestRequest(csrPEMExampleNotPresent),
|
|
))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
certPEMExampleNotPresent, _, err := pki.SignCertificate(tmpl, tmpl, skExampleNotPresent.Public(), skExampleNotPresent)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
baseOrder, err := new(ACME).buildOrder(baseCSR, req, baseIssuer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := map[string]struct {
|
|
builder *testpkg.Builder
|
|
csr *certificatesv1.CertificateSigningRequest
|
|
expectedErr bool
|
|
}{
|
|
"a CertificateSigningRequest without an approved condition should fire an event": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
|
ExpectedEvents: []string{
|
|
"Normal WaitingApproval Waiting for the Approved condition before issuing",
|
|
},
|
|
},
|
|
},
|
|
"a CertificateSigningRequest with a denied condition should do nothing": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateDenied,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
|
ExpectedEvents: []string{},
|
|
ExpectedActions: nil,
|
|
},
|
|
},
|
|
"an approved CSR that contains a garbage request should be marked as Failed": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestRequest([]byte("garbage-data")),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
|
ExpectedEvents: []string{
|
|
"Warning RequestParsingError Failed to decode CSR in spec.request: error decoding certificate request PEM block",
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
|
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
|
"status",
|
|
"",
|
|
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
|
gen.SetCertificateSigningRequestRequest([]byte("garbage-data")),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateFailed,
|
|
Status: corev1.ConditionTrue,
|
|
Reason: "RequestParsingError",
|
|
Message: "Failed to decode CSR in spec.request: error decoding certificate request PEM block",
|
|
LastTransitionTime: metaFixedClockStart,
|
|
LastUpdateTime: metaFixedClockStart,
|
|
}),
|
|
),
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the common name is not included in the DNS Names be marked as Failed": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestRequest(csrPEMExampleNotPresent),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
|
ExpectedEvents: []string{
|
|
`Warning InvalidOrder The CSR PEM requests a commonName that is not present in the list of dnsNames or ipAddresses. If a commonName is set, ACME requires that the value is also present in the list of dnsNames or ipAddresses: "example.com" does not exist in [foo.com] or []`,
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
|
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
|
"status",
|
|
"",
|
|
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
|
gen.SetCertificateSigningRequestRequest(csrPEMExampleNotPresent),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateFailed,
|
|
Status: corev1.ConditionTrue,
|
|
Reason: "InvalidOrder",
|
|
Message: `The CSR PEM requests a commonName that is not present in the list of dnsNames or ipAddresses. If a commonName is set, ACME requires that the value is also present in the list of dnsNames or ipAddresses: "example.com" does not exist in [foo.com] or []`,
|
|
LastTransitionTime: metaFixedClockStart,
|
|
LastUpdateTime: metaFixedClockStart,
|
|
}),
|
|
),
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR which contains a garbage duration and has duration enabled, should fail when parsing duration and be marked as Failed": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestDuration("garbage-data"),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{EnableDurationFeature: true}),
|
|
)},
|
|
ExpectedEvents: []string{
|
|
`Warning ErrorParseDuration Failed to parse requested duration: failed to parse requested duration on annotation "experimental.cert-manager.io/request-duration": time: invalid duration "garbage-data"`,
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
|
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
|
"status",
|
|
"",
|
|
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
|
gen.SetCertificateSigningRequestDuration("garbage-data"),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateFailed,
|
|
Status: corev1.ConditionTrue,
|
|
Reason: "ErrorParseDuration",
|
|
Message: `Failed to parse requested duration: failed to parse requested duration on annotation "experimental.cert-manager.io/request-duration": time: invalid duration "garbage-data"`,
|
|
LastTransitionTime: metaFixedClockStart,
|
|
LastUpdateTime: metaFixedClockStart,
|
|
}),
|
|
),
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order does not yet exist, should create the order and fire an event": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
)},
|
|
ExpectedEvents: []string{
|
|
`Normal OrderCreated Created Order resource default-unit-test-ns/test-csr-3290353799`,
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
cmacme.SchemeGroupVersion.WithResource("orders"),
|
|
gen.DefaultTestNamespace,
|
|
baseOrder,
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order already exists, but is owned by another CSR, return error": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
expectedErr: true,
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
),
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderOwnerReference(metav1.OwnerReference{}),
|
|
),
|
|
},
|
|
ExpectedEvents: []string{},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order already exists but is in a Failure state should mark the CSR and Failed": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
),
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderStatus(cmacme.OrderStatus{
|
|
Reason: "generic error",
|
|
State: cmacme.Invalid,
|
|
}),
|
|
),
|
|
},
|
|
ExpectedEvents: []string{
|
|
`Warning OrderFailed Failed to wait for order resource default-unit-test-ns/test-csr-3290353799 to become ready: order is in "invalid" state: generic error`,
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
|
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
|
"status",
|
|
"",
|
|
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateFailed,
|
|
Status: corev1.ConditionTrue,
|
|
Reason: "OrderFailed",
|
|
Message: `Failed to wait for order resource default-unit-test-ns/test-csr-3290353799 to become ready: order is in "invalid" state: generic error`,
|
|
LastTransitionTime: metaFixedClockStart,
|
|
LastUpdateTime: metaFixedClockStart,
|
|
}),
|
|
),
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order is not in a Valid state should fire an event and return": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
),
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderStatus(cmacme.OrderStatus{
|
|
Reason: "pending",
|
|
State: cmacme.Pending,
|
|
}),
|
|
),
|
|
},
|
|
ExpectedEvents: []string{
|
|
`Normal OrderPending Waiting on certificate issuance from order default-unit-test-ns/test-csr-3290353799: "pending"`,
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order is in a valid state, but the Certificate is empty should fire an event": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
),
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderStatus(cmacme.OrderStatus{
|
|
State: cmacme.Valid,
|
|
}),
|
|
gen.SetOrderCertificate(nil),
|
|
),
|
|
},
|
|
ExpectedEvents: []string{
|
|
"Normal OrderPending Waiting for order-controller to add certificate data to Order default-unit-test-ns/test-csr-3290353799",
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order is in a valid state, but the certificate is garbage, should delete the Order and fire an event": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
),
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderStatus(cmacme.OrderStatus{
|
|
State: cmacme.Valid,
|
|
}),
|
|
gen.SetOrderCertificate([]byte("garbage-data")),
|
|
),
|
|
},
|
|
ExpectedEvents: []string{
|
|
"Warning OrderBadCertificate Deleting Order with bad certificate: error decoding certificate PEM block",
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewDeleteAction(
|
|
cmacme.SchemeGroupVersion.WithResource("orders"),
|
|
gen.DefaultTestNamespace,
|
|
baseOrder.Name,
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order is in a valid state, but the certificate is singed for a different key than the request, delete the order": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
),
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderStatus(cmacme.OrderStatus{
|
|
State: cmacme.Valid,
|
|
}),
|
|
gen.SetOrderCertificate(certPEMExampleNotPresent),
|
|
),
|
|
},
|
|
ExpectedEvents: []string{
|
|
"Warning OrderBadCertificate Deleting Order as the signed certificate's key does not match the request",
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewDeleteAction(
|
|
cmacme.SchemeGroupVersion.WithResource("orders"),
|
|
gen.DefaultTestNamespace,
|
|
baseOrder.Name,
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
"an approved CSR where the order is in a valid state, should update the CSR with the Certificate": {
|
|
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
),
|
|
builder: &testpkg.Builder{
|
|
CertManagerObjects: []runtime.Object{
|
|
gen.IssuerFrom(baseIssuer.DeepCopy(),
|
|
gen.SetIssuerACME(cmacme.ACMEIssuer{}),
|
|
),
|
|
gen.OrderFrom(baseOrder,
|
|
gen.SetOrderStatus(cmacme.OrderStatus{
|
|
State: cmacme.Valid,
|
|
Certificate: certPEM,
|
|
}),
|
|
gen.SetOrderCertificate(certPEM),
|
|
),
|
|
},
|
|
ExpectedEvents: []string{
|
|
"Normal CertificateIssued Certificate fetched from issuer successfully",
|
|
},
|
|
ExpectedActions: []testpkg.Action{
|
|
testpkg.NewAction(coretesting.NewCreateAction(
|
|
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
|
"",
|
|
&authzv1.SubjectAccessReview{
|
|
Spec: authzv1.SubjectAccessReviewSpec{
|
|
User: "user-1",
|
|
Groups: []string{"group-1", "group-2"},
|
|
Extra: map[string]authzv1.ExtraValue{
|
|
"extra": []string{"1", "2"},
|
|
},
|
|
UID: "uid-1",
|
|
|
|
ResourceAttributes: &authzv1.ResourceAttributes{
|
|
Group: certmanager.GroupName,
|
|
Resource: "signers",
|
|
Verb: "reference",
|
|
Namespace: baseIssuer.Namespace,
|
|
Name: baseIssuer.Name,
|
|
Version: "*",
|
|
},
|
|
},
|
|
},
|
|
)),
|
|
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
|
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
|
"status",
|
|
"",
|
|
gen.CertificateSigningRequestFrom(baseCSR,
|
|
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Status: corev1.ConditionTrue,
|
|
}),
|
|
gen.SetCertificateSigningRequestCertificate(certPEM),
|
|
),
|
|
)),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
if test.csr != nil {
|
|
test.builder.KubeObjects = append(test.builder.KubeObjects, test.csr)
|
|
}
|
|
|
|
fixedClock.SetTime(fixedClockStart)
|
|
test.builder.Clock = fixedClock
|
|
test.builder.T = t
|
|
test.builder.Init()
|
|
|
|
// Always return true for SubjectAccessReviews in tests
|
|
test.builder.FakeKubeClient().PrependReactor("create", "*", func(action coretesting.Action) (bool, runtime.Object, error) {
|
|
if action.GetResource() != authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews") {
|
|
return false, nil, nil
|
|
}
|
|
return true, &authzv1.SubjectAccessReview{
|
|
Status: authzv1.SubjectAccessReviewStatus{
|
|
Allowed: true,
|
|
},
|
|
}, nil
|
|
})
|
|
|
|
defer test.builder.Stop()
|
|
|
|
controller := controllerBuilder()
|
|
controller.Register(test.builder.Context)
|
|
|
|
test.builder.Start()
|
|
|
|
err := controller.ProcessItem(context.Background(), test.csr.Name)
|
|
if (err != nil) != test.expectedErr {
|
|
t.Errorf("unexpected error, exp=%t got=%v", test.expectedErr, err)
|
|
}
|
|
|
|
test.builder.CheckAndFinish(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_buildOrder(t *testing.T) {
|
|
csrPEM, _, err := gen.CSR(x509.ECDSA,
|
|
gen.SetCSRCommonName("example.com"),
|
|
gen.SetCSRDNSNames("example.com"),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req, err := pki.DecodeX509CertificateRequestBytes(csrPEM)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
csr := gen.CertificateSigningRequest("test",
|
|
gen.SetCertificateSigningRequestDuration("1h"),
|
|
gen.SetCertificateSigningRequestRequest(csrPEM),
|
|
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/test-ns.test-name"),
|
|
)
|
|
|
|
tests := map[string]struct {
|
|
enableDurationFeature bool
|
|
|
|
want *cmacme.Order
|
|
wantErr bool
|
|
}{
|
|
"Normal building of order": {
|
|
enableDurationFeature: false,
|
|
want: &cmacme.Order{
|
|
Spec: cmacme.OrderSpec{
|
|
Request: csrPEM,
|
|
CommonName: "example.com",
|
|
DNSNames: []string{"example.com"},
|
|
IssuerRef: cmmeta.ObjectReference{
|
|
Name: "test-name",
|
|
Kind: "Issuer",
|
|
Group: "cert-manager.io",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
"Building with enableDurationFeature": {
|
|
enableDurationFeature: true,
|
|
want: &cmacme.Order{
|
|
Spec: cmacme.OrderSpec{
|
|
Request: csrPEM,
|
|
CommonName: "example.com",
|
|
DNSNames: []string{"example.com"},
|
|
Duration: &metav1.Duration{Duration: time.Hour},
|
|
IssuerRef: cmmeta.ObjectReference{
|
|
Name: "test-name",
|
|
Kind: "Issuer",
|
|
Group: "cert-manager.io",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
got, err := new(ACME).buildOrder(csr, req, &cmapi.Issuer{
|
|
Spec: cmapi.IssuerSpec{
|
|
IssuerConfig: cmapi.IssuerConfig{
|
|
ACME: &cmacme.ACMEIssuer{
|
|
EnableDurationFeature: test.enableDurationFeature,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("buildOrder() error = %v, wantErr %v", err, test.wantErr)
|
|
return
|
|
}
|
|
|
|
// for the current purpose we only test the spec
|
|
if !reflect.DeepEqual(got.Spec, test.want.Spec) {
|
|
t.Errorf("buildOrder() got = %v, want %v", got.Spec, test.want.Spec)
|
|
}
|
|
})
|
|
}
|
|
|
|
longCSROne := gen.CertificateSigningRequest(
|
|
"test-comparison-that-is-at-the-fifty-two-character-l",
|
|
gen.SetCertificateSigningRequestDuration("1h"),
|
|
gen.SetCertificateSigningRequestRequest(csrPEM),
|
|
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/test-ns.test-name"),
|
|
)
|
|
orderOne, err := new(ACME).buildOrder(longCSROne, req, gen.Issuer("test-name", gen.SetIssuerACME(cmacme.ACMEIssuer{})))
|
|
if err != nil {
|
|
t.Errorf("buildOrder() received error %v", err)
|
|
return
|
|
}
|
|
|
|
t.Run("Builds two orders from different long CSRs to guarantee unique name", func(t *testing.T) {
|
|
longCSRTwo := gen.CertificateSigningRequest(
|
|
"test-comparison-that-is-at-the-fifty-two-character-l-two",
|
|
gen.SetCertificateSigningRequestDuration("1h"),
|
|
gen.SetCertificateSigningRequestRequest(csrPEM),
|
|
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/test-ns.test-name"),
|
|
)
|
|
|
|
orderTwo, err := new(ACME).buildOrder(longCSRTwo, req, gen.Issuer("test-name", gen.SetIssuerACME(cmacme.ACMEIssuer{})))
|
|
if err != nil {
|
|
t.Errorf("buildOrder() received error %v", err)
|
|
return
|
|
}
|
|
|
|
if orderOne.Name == orderTwo.Name {
|
|
t.Errorf(
|
|
"orders built from different CSRs have equal names: %s == %s",
|
|
orderOne.Name,
|
|
orderTwo.Name)
|
|
}
|
|
})
|
|
|
|
t.Run("Builds two orders from the same long CSRs to guarantee same name", func(t *testing.T) {
|
|
orderOne, err := new(ACME).buildOrder(longCSROne, req, gen.Issuer("test-name", gen.SetIssuerACME(cmacme.ACMEIssuer{})))
|
|
if err != nil {
|
|
t.Errorf("buildOrder() received error %v", err)
|
|
return
|
|
}
|
|
|
|
orderTwo, err := new(ACME).buildOrder(longCSROne, req, gen.Issuer("test-name", gen.SetIssuerACME(cmacme.ACMEIssuer{})))
|
|
if err != nil {
|
|
t.Errorf("buildOrder() received error %v", err)
|
|
return
|
|
}
|
|
if orderOne.Name != orderTwo.Name {
|
|
t.Errorf(
|
|
"orders built from the same CSR have unequal names: %s != %s",
|
|
orderOne.Name,
|
|
orderTwo.Name)
|
|
}
|
|
})
|
|
}
|