cert-manager/pkg/controller/acmeorders/sync_test.go
irbekrm dba18119aa Ensures that key for an ACME challenge is only retrieved from the ACME server once
Thus reducing the number of HTTP01ChallengeResponse/DNS01ChallengeResponse calls

Signed-off-by: irbekrm <irbekrm@gmail.com>
2023-04-05 16:28:14 +01:00

844 lines
30 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 acmeorders
import (
"context"
"encoding/pem"
"errors"
"fmt"
"testing"
"time"
acmeapi "golang.org/x/crypto/acme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
coretesting "k8s.io/client-go/testing"
fakeclock "k8s.io/utils/clock/testing"
accountstest "github.com/cert-manager/cert-manager/pkg/acme/accounts/test"
acmecl "github.com/cert-manager/cert-manager/pkg/acme/client"
cmacme "github.com/cert-manager/cert-manager/pkg/apis/acme/v1"
cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
testpkg "github.com/cert-manager/cert-manager/pkg/controller/test"
schedulertest "github.com/cert-manager/cert-manager/pkg/scheduler/test"
"github.com/cert-manager/cert-manager/test/unit/gen"
)
func TestSync(t *testing.T) {
nowTime := time.Now()
nowMetaTime := metav1.NewTime(nowTime)
fixedClock := fakeclock.NewFakeClock(nowTime)
testIssuerHTTP01 := gen.Issuer("testissuer", gen.SetIssuerACME(cmacme.ACMEIssuer{
Solvers: []cmacme.ACMEChallengeSolver{
{
HTTP01: &cmacme.ACMEChallengeSolverHTTP01{
Ingress: &cmacme.ACMEChallengeSolverHTTP01Ingress{},
},
},
},
}))
testIssuerHTTP01TestCom := gen.Issuer("testissuer", gen.SetIssuerACME(cmacme.ACMEIssuer{
Solvers: []cmacme.ACMEChallengeSolver{
{
Selector: &cmacme.CertificateDNSNameSelector{
DNSNames: []string{"test.com"},
},
HTTP01: &cmacme.ACMEChallengeSolverHTTP01{
Ingress: &cmacme.ACMEChallengeSolverHTTP01Ingress{},
},
},
},
}))
testIssuerHTTP01TestComPreferredChain := gen.Issuer("testissuer", gen.SetIssuerACME(cmacme.ACMEIssuer{
PreferredChain: "ISRG Root X1",
Solvers: []cmacme.ACMEChallengeSolver{
{
Selector: &cmacme.CertificateDNSNameSelector{
DNSNames: []string{"test.com"},
},
HTTP01: &cmacme.ACMEChallengeSolverHTTP01{
Ingress: &cmacme.ACMEChallengeSolverHTTP01Ingress{},
},
},
},
}))
testOrder := gen.Order("testorder",
gen.SetOrderCommonName("test.com"),
gen.SetOrderIssuer(cmmeta.ObjectReference{
Name: testIssuerHTTP01TestCom.Name,
}),
)
testOrderIP := gen.Order("testorder", gen.SetOrderIssuer(cmmeta.ObjectReference{Name: testIssuerHTTP01.Name}), gen.SetOrderIPAddresses("10.0.0.1"))
pendingStatus := cmacme.OrderStatus{
State: cmacme.Pending,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
Identifier: "test.com",
Challenges: []cmacme.ACMEChallenge{
{
URL: "http://chalurl",
Token: "token",
Type: "http-01",
},
},
},
},
}
erroredStatus := cmacme.OrderStatus{
State: cmacme.Errored,
}
erroredStatusWithDetail := cmacme.OrderStatus{
State: cmacme.Errored,
FailureTime: &nowMetaTime,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Reason: "Failed to finalize Order: 429 : some error",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
Identifier: "test.com",
InitialState: cmacme.Valid,
Challenges: []cmacme.ACMEChallenge{
{
URL: "http://chalurl",
Token: "token",
Type: "http-01",
},
},
},
},
}
acmeError429 := acmeapi.Error{
StatusCode: 429,
Detail: "some error",
}
acmeError403 := acmeapi.Error{
StatusCode: 403,
Detail: "some error",
}
testOrderPending := gen.OrderFrom(testOrder, gen.SetOrderStatus(pendingStatus))
testOrderInvalid := testOrderPending.DeepCopy()
testOrderInvalid.Status.State = cmacme.Invalid
testOrderInvalid.Status.FailureTime = &nowMetaTime
testOrderErrored := gen.OrderFrom(testOrder, gen.SetOrderStatus(erroredStatus))
testOrderErrored.Status.FailureTime = &nowMetaTime
testOrderErroredWithDetail := gen.OrderFrom(testOrderPending, gen.SetOrderStatus(erroredStatusWithDetail))
testOrderValid := testOrderPending.DeepCopy()
testOrderValid.Status.State = cmacme.Valid
// pem encoded word 'test'
testOrderValid.Status.Certificate = []byte(`-----BEGIN CERTIFICATE-----
dGVzdA==
-----END CERTIFICATE-----
`)
testOrderReady := testOrderPending.DeepCopy()
testOrderReady.Status.State = cmacme.Ready
testCert := []byte(`-----BEGIN CERTIFICATE-----
MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1
WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX
NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf
89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl
Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc
Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz
uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB
AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU
BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB
FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo
SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js
LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF
BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG
AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD
VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB
ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx
A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM
UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2
DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1
eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu
OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw
p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY
2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0
ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR
PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b
rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt
-----END CERTIFICATE-----
`)
rawTestCert, _ := pem.Decode(testCert)
testOrderValidAltCert := gen.OrderFrom(testOrder, gen.SetOrderStatus(pendingStatus))
testOrderValidAltCert.Status.State = cmacme.Valid
testOrderValidAltCert.Status.Certificate = testCert
fakeHTTP01ACMECl := &acmecl.FakeACME{
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
}
testAuthorizationChallenge, err := buildPartialChallenge(context.TODO(), testIssuerHTTP01TestCom, testOrderPending, testOrderPending.Status.Authorizations[0])
if err != nil {
t.Fatalf("error building Challenge resource test fixture: %v", err)
}
key, err := fakeHTTP01ACMECl.FakeHTTP01ChallengeResponse(testAuthorizationChallenge.Spec.Token)
if err != nil {
t.Fatalf("error building Challenge resource test fixture: %v", err)
}
testAuthorizationChallenge.Spec.Key = key
testAuthorizationChallengeValid := testAuthorizationChallenge.DeepCopy()
testAuthorizationChallengeValid.Status.State = cmacme.Valid
testAuthorizationChallengeInvalid := testAuthorizationChallenge.DeepCopy()
testAuthorizationChallengeInvalid.Status.State = cmacme.Invalid
testACMEAuthorizationPending := &acmeapi.Authorization{
URI: "http://authzurl",
Status: acmeapi.StatusPending,
Identifier: acmeapi.AuthzID{
Value: "test.com",
},
Challenges: []*acmeapi.Challenge{
{
Type: "http-01",
Token: "token",
},
},
}
testACMEOrderPending := &acmeapi.Order{
URI: testOrderPending.Status.URL,
Identifiers: []acmeapi.AuthzID{
{
Type: "dns",
Value: "test.com",
},
},
FinalizeURL: testOrderPending.Status.FinalizeURL,
AuthzURLs: []string{"http://authzurl"},
Status: acmeapi.StatusPending,
}
// shallow copy
testACMEOrderValid := &acmeapi.Order{}
*testACMEOrderValid = *testACMEOrderPending
testACMEOrderValid.Status = acmeapi.StatusValid
// shallow copy
testACMEOrderReady := &acmeapi.Order{}
*testACMEOrderReady = *testACMEOrderPending
testACMEOrderReady.Status = acmeapi.StatusReady
// shallow copy
testACMEOrderInvalid := &acmeapi.Order{}
*testACMEOrderInvalid = *testACMEOrderPending
testACMEOrderInvalid.Status = acmeapi.StatusInvalid
tests := map[string]testT{
"create a new order with the acme server, set the order url on the status resource and return nil to avoid cache timing issues": {
order: testOrder,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrder},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderPending.Namespace,
gen.OrderFrom(testOrder, gen.SetOrderStatus(cmacme.OrderStatus{
State: cmacme.Pending,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
},
},
})))),
},
},
acmeClient: &acmecl.FakeACME{
FakeAuthorizeOrder: func(ctx context.Context, id []acmeapi.AuthzID, opt ...acmeapi.OrderOption) (*acmeapi.Order, error) {
return testACMEOrderPending, nil
},
FakeGetAuthorization: func(ctx context.Context, url string) (*acmeapi.Authorization, error) {
if url != "http://authzurl" {
return nil, fmt.Errorf("Invalid URL: expected http://authzurl got %q", url)
}
return testACMEAuthorizationPending, nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"create a new order with the acme server with an IP address": {
order: testOrderIP,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01, testOrderIP},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderPending.Namespace,
gen.OrderFrom(testOrderIP, gen.SetOrderStatus(cmacme.OrderStatus{
State: cmacme.Pending,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
},
},
})))),
},
},
acmeClient: &acmecl.FakeACME{
FakeAuthorizeOrder: func(ctx context.Context, id []acmeapi.AuthzID, opt ...acmeapi.OrderOption) (*acmeapi.Order, error) {
if id[0].Value != "10.0.0.1" || id[0].Type != "ip" {
return nil, errors.New("AuthzID needs to be the IP")
}
return testACMEOrderPending, nil
},
FakeGetAuthorization: func(ctx context.Context, url string) (*acmeapi.Authorization, error) {
if url != "http://authzurl" {
return nil, fmt.Errorf("Invalid URL: expected http://authzurl got %q", url)
}
return testACMEAuthorizationPending, nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"create a challenge resource for the test.com dnsName on the order": {
order: testOrderPending,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderPending},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewCreateAction(cmacme.SchemeGroupVersion.WithResource("challenges"), testAuthorizationChallenge.Namespace, testAuthorizationChallenge)),
},
ExpectedEvents: []string{
`Normal Created Created Challenge resource "testorder-756011405" for domain "test.com"`,
},
},
acmeClient: &acmecl.FakeACME{
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"should refuse to create a challenge if only an unknown challenge type is offered": {
order: gen.OrderFrom(testOrderPending, gen.SetOrderStatus(cmacme.OrderStatus{
State: cmacme.Pending,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
Identifier: "test.com",
Challenges: []cmacme.ACMEChallenge{
{
URL: "http://chalurl",
Token: "token",
Type: "unknown-type",
},
},
},
},
})),
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{
testIssuerHTTP01TestCom,
},
ExpectedEvents: []string{
// the 'unsupported challenge type' text is not printed here as the code that 'selects'
// a solver to use for a challenge filters out unsupported challenge types earlier
// in its selection routine.
`Warning Solver Failed to determine a valid solver configuration for the set of domains on the Order: no configured challenge solvers can be used for this challenge`,
},
},
},
"skip creating a Challenge for an already valid authorization, reschedule if the ACME Order is still pending": {
order: gen.OrderFrom(testOrder, gen.SetOrderStatus(
cmacme.OrderStatus{
State: cmacme.Pending,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
Identifier: "test.com",
InitialState: cmacme.Valid,
Challenges: []cmacme.ACMEChallenge{
{
URL: "http://chalurl",
Token: "token",
Type: "http-01",
},
},
},
},
},
)),
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderPending},
ExpectedActions: []testpkg.Action{},
ExpectedEvents: []string{},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(ctx context.Context, url string) (*acmeapi.Order, error) {
return &acmeapi.Order{
URI: "http://testurl.com/abcde",
Status: acmeapi.StatusPending,
FinalizeURL: "http://testurl.com/abcde/finalize",
CertURL: "",
}, nil
},
},
shouldSchedule: true,
},
"skip creating a Challenge for an already valid authorization": {
order: gen.OrderFrom(testOrder, gen.SetOrderStatus(
cmacme.OrderStatus{
State: cmacme.Pending,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
Identifier: "test.com",
InitialState: cmacme.Valid,
Challenges: []cmacme.ACMEChallenge{
{
URL: "http://chalurl",
Token: "token",
Type: "http-01",
},
},
},
},
},
)),
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderPending},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrder.Namespace, gen.OrderFrom(testOrder, gen.SetOrderStatus(
cmacme.OrderStatus{
// The 'state' field should be updated to reflect the
// Order returned by FakeGetOrder
State: cmacme.Valid,
URL: "http://testurl.com/abcde",
FinalizeURL: "http://testurl.com/abcde/finalize",
Authorizations: []cmacme.ACMEAuthorization{
{
URL: "http://authzurl",
Identifier: "test.com",
InitialState: cmacme.Valid,
Challenges: []cmacme.ACMEChallenge{
{
URL: "http://chalurl",
Token: "token",
Type: "http-01",
},
},
},
},
},
)),
)),
},
ExpectedEvents: []string{},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(ctx context.Context, url string) (*acmeapi.Order, error) {
return &acmeapi.Order{
URI: "http://testurl.com/abcde",
Status: acmeapi.StatusValid,
FinalizeURL: "http://testurl.com/abcde/finalize",
CertURL: "",
}, nil
},
},
},
"do nothing if the challenge for test.com is still pending": {
order: testOrderPending,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderPending, testAuthorizationChallenge},
ExpectedActions: []testpkg.Action{},
},
acmeClient: &acmecl.FakeACME{
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"call GetOrder and update the order state to 'ready' if all challenges are 'valid'": {
order: testOrderPending,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderPending, testAuthorizationChallengeValid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderReady.Namespace, testOrderReady)),
},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderReady, nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"call FinalizeOrder and update the order state to 'valid' if finalize succeeds": {
order: testOrderReady,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderReady, testAuthorizationChallengeValid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderValid.Namespace, testOrderValid)),
},
ExpectedEvents: []string{
"Normal Complete Order completed successfully",
},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderValid, nil
},
FakeCreateOrderCert: func(_ context.Context, url string, csr []byte, bundle bool) ([][]byte, string, error) {
testData := []byte("test")
return [][]byte{testData}, "http://testurl", nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"call FinalizeOrder and update the order state to 'errored' if finalize fails with a 4xx ACME error": {
order: gen.OrderFrom(testOrderErroredWithDetail, gen.SetOrderState(cmacme.Ready)),
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, gen.OrderFrom(testOrderErroredWithDetail, gen.SetOrderState(cmacme.Ready))},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderErroredWithDetail.Namespace, testOrderErroredWithDetail)),
},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderReady, nil
},
FakeCreateOrderCert: func(_ context.Context, url string, csr []byte, bundle bool) ([][]byte, string, error) {
return nil, "", &acmeError429
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"call FinalizeOrder, return error if finalize fails with an unspecified error": {
order: gen.OrderFrom(testOrderErroredWithDetail, gen.SetOrderState(cmacme.Ready)),
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, gen.OrderFrom(testOrderErroredWithDetail, gen.SetOrderState(cmacme.Ready))},
ExpectedActions: []testpkg.Action{},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderReady, nil
},
FakeCreateOrderCert: func(_ context.Context, url string, csr []byte, bundle bool) ([][]byte, string, error) {
return nil, "", errors.New("some error")
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
expectErr: true,
},
"call FinalizeOrder, recover if finalize fails because order is already finalized": {
order: testOrderReady,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderReady, testAuthorizationChallengeValid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderValid.Namespace, testOrderValid)),
},
ExpectedEvents: []string{
"Normal Complete Order completed successfully",
},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderValid, nil
},
FakeCreateOrderCert: func(_ context.Context, url string, csr []byte, bundle bool) ([][]byte, string, error) {
return nil, "", &acmeError403
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
FakeFetchCert: func(_ context.Context, url string, bundle bool) ([][]byte, error) {
return [][]byte{[]byte("test")}, nil
},
},
expectErr: false,
},
"call FinalizeOrder, recover if finalize fails because order is already finalized and fetch alternate cert chain": {
order: testOrderReady,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestComPreferredChain, testOrderReady, testAuthorizationChallengeValid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderValid.Namespace, testOrderValidAltCert)),
},
ExpectedEvents: []string{
"Normal Complete Order completed successfully",
},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderValid, nil
},
FakeCreateOrderCert: func(_ context.Context, url string, csr []byte, bundle bool) ([][]byte, string, error) {
return nil, "", &acmeError403
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
FakeListCertAlternates: func(_ context.Context, url string) ([]string, error) {
return []string{"http://alturl"}, nil
},
FakeFetchCert: func(_ context.Context, url string, bundle bool) ([][]byte, error) {
if url != "http://alturl" {
// This bit just ensures that we
// call it from the correct
// place. This is the same URL
// that is returned from
// FakeCertAlternates that
// should have been called
// before this.
return nil, errors.New("Cert URL is incorrect")
}
if !bundle {
return nil, errors.New("Expecting to be called with bundle=true")
}
return [][]byte{rawTestCert.Bytes}, nil
},
},
expectErr: false,
},
"call FinalizeOrder fetch alternate cert chain": {
order: testOrderReady.DeepCopy(),
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestComPreferredChain, testOrderReady, testAuthorizationChallengeValid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderValid.Namespace, testOrderValidAltCert)),
},
ExpectedEvents: []string{
"Normal Complete Order completed successfully",
},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderValid, nil
},
FakeCreateOrderCert: func(_ context.Context, url string, csr []byte, bundle bool) ([][]byte, string, error) {
testData := []byte("test")
return [][]byte{testData}, "http://testurl", nil
},
FakeListCertAlternates: func(_ context.Context, url string) ([]string, error) {
if url != "http://testurl" {
return nil, errors.New("Cert URL is incorrect")
}
return []string{"http://alturl"}, nil
},
FakeFetchCert: func(_ context.Context, url string, bundle bool) ([][]byte, error) {
if url != "http://alturl" {
// This bit just ensures that we
// call it from the correct
// place. This is the same URL
// that is returned from
// FakeCertAlternates that
// should have been called
// before this.
return nil, errors.New("Cert URL is incorrect")
}
if !bundle {
return nil, errors.New("Expecting to be called with bundle=true")
}
return [][]byte{rawTestCert.Bytes}, nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"call GetOrder and update the order state if the challenge is 'failed'": {
order: testOrderPending,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderPending, testAuthorizationChallengeInvalid},
ExpectedActions: []testpkg.Action{
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(cmacme.SchemeGroupVersion.WithResource("orders"),
"status",
testOrderInvalid.Namespace, testOrderInvalid)),
},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderInvalid, nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
return "key", nil
},
},
},
"should leave the order state as-is if the challenge is marked invalid but the acme order is pending": {
order: testOrderPending,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderPending, testAuthorizationChallengeInvalid},
ExpectedActions: []testpkg.Action{},
},
acmeClient: &acmecl.FakeACME{
FakeGetOrder: func(_ context.Context, url string) (*acmeapi.Order, error) {
return testACMEOrderPending, nil
},
FakeHTTP01ChallengeResponse: func(s string) (string, error) {
// TODO: assert s = "token"
return "key", nil
},
},
},
"do nothing if the order is valid": {
order: testOrderValid,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderValid},
ExpectedActions: []testpkg.Action{},
},
acmeClient: &acmecl.FakeACME{},
},
"do nothing if the order is invalid": {
order: testOrderInvalid,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderInvalid},
ExpectedActions: []testpkg.Action{},
},
acmeClient: &acmecl.FakeACME{},
},
"do nothing if the order is in errored state with no url or finalize url on status": {
order: testOrderErrored,
builder: &testpkg.Builder{
CertManagerObjects: []runtime.Object{testIssuerHTTP01TestCom, testOrderErrored},
ExpectedActions: []testpkg.Action{},
},
acmeClient: &acmecl.FakeACME{},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
// reset the fixedClock at the start of each test
fixedClock.SetTime(nowTime)
// always use the fixedClock unless otherwise specified
if test.builder.Clock == nil {
test.builder.Clock = fixedClock
}
runTest(t, test)
})
}
}
type testT struct {
order *cmacme.Order
builder *testpkg.Builder
acmeClient acmecl.Interface
shouldSchedule bool
expectErr bool
}
func runTest(t *testing.T, test testT) {
test.builder.T = t
test.builder.Init()
defer test.builder.Stop()
cw := &controllerWrapper{}
_, _, err := cw.Register(test.builder.Context)
if err != nil {
t.Errorf("Error registering the controller: %v", err)
}
// Set some fields on the embedded controller.
cw.accountRegistry = &accountstest.FakeRegistry{
GetClientFunc: func(_ string) (acmecl.Interface, error) {
return test.acmeClient, nil
},
}
gotScheduled := false
fakeScheduler := schedulertest.FakeScheduler{
AddFunc: func(obj interface{}, duration time.Duration) {
gotScheduled = true
},
}
cw.scheduledWorkQueue = &fakeScheduler
test.builder.Start()
err = cw.Sync(context.Background(), test.order)
if err != nil && !test.expectErr {
t.Errorf("Expected function to not error, but got: %v", err)
}
if err == nil && test.expectErr {
t.Errorf("Expected function to get an error, but got: %v", err)
}
if gotScheduled != test.shouldSchedule {
t.Errorf("Expected Order to be re-queued: %v got re-queued: %v", test.shouldSchedule, gotScheduled)
}
test.builder.CheckAndFinish(err)
}