diff --git a/cmd/controller/app/BUILD.bazel b/cmd/controller/app/BUILD.bazel index d2040c216..5d9b242bd 100644 --- a/cmd/controller/app/BUILD.bazel +++ b/cmd/controller/app/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "//pkg/client/informers/externalversions:go_default_library", "//pkg/controller:go_default_library", "//pkg/controller/certificaterequests/ca:go_default_library", + "//pkg/controller/certificates:go_default_library", "//pkg/controller/clusterissuers:go_default_library", "//pkg/feature:go_default_library", "//pkg/issuer/acme/dns/util:go_default_library", diff --git a/cmd/controller/app/controller.go b/cmd/controller/app/controller.go index 1fffd593d..eb0a5b3bc 100644 --- a/cmd/controller/app/controller.go +++ b/cmd/controller/app/controller.go @@ -42,6 +42,7 @@ import ( informers "github.com/jetstack/cert-manager/pkg/client/informers/externalversions" "github.com/jetstack/cert-manager/pkg/controller" cacertificaterequestcontroller "github.com/jetstack/cert-manager/pkg/controller/certificaterequests/ca" + certificatescontroller "github.com/jetstack/cert-manager/pkg/controller/certificates" "github.com/jetstack/cert-manager/pkg/controller/clusterissuers" "github.com/jetstack/cert-manager/pkg/feature" dnsutil "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util" @@ -77,6 +78,7 @@ func Run(opts *options.ControllerOptions, stopCh <-chan struct{}) { if utilfeature.DefaultFeatureGate.Enabled(feature.CertificateRequestControllers) { opts.EnabledControllers = append(opts.EnabledControllers, []string{ cacertificaterequestcontroller.CRControllerName, + certificatescontroller.ExperimentalControllerName, }...) } diff --git a/pkg/controller/certificates/BUILD.bazel b/pkg/controller/certificates/BUILD.bazel index 1b78af004..a34abb766 100644 --- a/pkg/controller/certificates/BUILD.bazel +++ b/pkg/controller/certificates/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "certificate_request.go", "checks.go", "controller.go", "sync.go", @@ -11,7 +12,6 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/api/util:go_default_library", - "//pkg/apis/certmanager:go_default_library", "//pkg/apis/certmanager/v1alpha1:go_default_library", "//pkg/apis/certmanager/validation:go_default_library", "//pkg/client/clientset/versioned:go_default_library", @@ -60,6 +60,7 @@ filegroup( go_test( name = "go_default_test", srcs = [ + "certificate_request_test.go", "sync_test.go", "util_test.go", ], diff --git a/pkg/controller/certificates/certificate_request.go b/pkg/controller/certificates/certificate_request.go new file mode 100644 index 000000000..e6d22fbeb --- /dev/null +++ b/pkg/controller/certificates/certificate_request.go @@ -0,0 +1,931 @@ +/* +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 certificates + +import ( + "context" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "hash/fnv" + "reflect" + "strings" + "time" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" + + apiutil "github.com/jetstack/cert-manager/pkg/api/util" + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" + cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha1" + controllerpkg "github.com/jetstack/cert-manager/pkg/controller" + "github.com/jetstack/cert-manager/pkg/feature" + logf "github.com/jetstack/cert-manager/pkg/logs" + "github.com/jetstack/cert-manager/pkg/metrics" + "github.com/jetstack/cert-manager/pkg/scheduler" + "github.com/jetstack/cert-manager/pkg/util/errors" + "github.com/jetstack/cert-manager/pkg/util/kube" + "github.com/jetstack/cert-manager/pkg/util/pki" +) + +// certificateRequestManager manages CertificateRequest resources for a +// Certificate in order to obtain signed certs. +type certificateRequestManager struct { + certificateLister cmlisters.CertificateLister + secretLister corelisters.SecretLister + certificateRequestLister cmlisters.CertificateRequestLister + + kubeClient kubernetes.Interface + cmClient cmclient.Interface + + // maintain a reference to the workqueue for this controller + // so the handleOwnedResource method can enqueue resources + queue workqueue.RateLimitingInterface + scheduledWorkQueue scheduler.ScheduledWorkQueue + + // used to record Events about resources to the API + recorder record.EventRecorder + + // used for testing + clock clock.Clock + + // defined as a field to make it easy to stub out for testing purposes + generatePrivateKeyBytes generatePrivateKeyBytesFn + generateCSR generateCSRFn + + // certificateNeedsRenew is a function that can be used to determine whether + // a certificate currently requires renewal. + // This is a field on the controller struct to avoid having to maintain a reference + // to the controller context, and to make it easier to fake out this call during tests. + certificateNeedsRenew func(ctx context.Context, cert *x509.Certificate, crt *cmapi.Certificate) bool + + // calculateDurationUntilRenew returns the amount of time before the controller should + // begin attempting to renew the certificate, given the provided existing certificate + // and certificate spec. + // This is a field on the controller struct to avoid having to maintain a reference + // to the controller context, and to make it easier to fake out this call during tests. + calculateDurationUntilRenew calculateDurationUntilRenewFn + + // localTemporarySigner signs a certificate that is stored temporarily + localTemporarySigner localTemporarySignerFn + + // issueTemporaryCerts gates whether temporary certificates should be issued. + // This is defined here as a bool to make it easy to disable this behaviour. + issueTemporaryCerts bool +} + +type localTemporarySignerFn func(crt *cmapi.Certificate, pk []byte) ([]byte, error) + +// Register registers and constructs the controller using the provided context. +// It returns the workqueue to be used to enqueue items, a list of +// InformerSynced functions that must be synced, or an error. +func (c *certificateRequestManager) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + log := logf.FromContext(ctx.RootContext, ExperimentalControllerName) + + // create a queue used to queue up items to be processed + c.queue = workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*5, time.Minute*30), ExperimentalControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := ctx.SharedInformerFactory.Certmanager().V1alpha1().Certificates() + certificateRequestInformer := ctx.SharedInformerFactory.Certmanager().V1alpha1().CertificateRequests() + secretsInformer := ctx.KubeSharedInformerFactory.Core().V1().Secrets() + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + certificateRequestInformer.Informer().HasSynced, + secretsInformer.Informer().HasSynced, + certificateInformer.Informer().HasSynced, + } + + // set all the references to the listers for used by the Sync function + c.certificateRequestLister = certificateRequestInformer.Lister() + c.secretLister = secretsInformer.Lister() + c.certificateLister = certificateInformer.Lister() + + // register handler functions + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: controllerpkg.HandleOwnedResourceNamespacedFunc(log, c.queue, certificateGvk, certificateGetter(c.certificateLister))}) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: secretResourceHandler(log, c.certificateLister, c.queue)}) + + // Create a scheduled work queue that calls the ctrl.queue.Add method for + // each object in the queue. This is used to schedule re-checks of + // Certificate resources when they get near to expiry + c.scheduledWorkQueue = scheduler.NewScheduledWorkQueue(c.queue.Add) + + // clock is used to determine whether certificates need renewal + c.clock = clock.RealClock{} + + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + + c.certificateNeedsRenew = ctx.IssuerOptions.CertificateNeedsRenew + c.calculateDurationUntilRenew = ctx.IssuerOptions.CalculateDurationUntilRenew + c.generatePrivateKeyBytes = generatePrivateKeyBytesImpl + c.generateCSR = generateCSRImpl + // the localTemporarySigner is used to sign 'temporary certificates' during + // asynchronous certificate issuance flows + c.localTemporarySigner = generateLocallySignedTemporaryCertificate + c.issueTemporaryCerts = utilfeature.DefaultFeatureGate.Enabled(feature.IssueTemporaryCertificate) + + c.cmClient = ctx.CMClient + c.kubeClient = ctx.Client + + return c.queue, mustSync, nil +} + +func (c *certificateRequestManager) ProcessItem(ctx context.Context, key string) error { + ctx = logf.NewContext(ctx, nil, ExperimentalControllerName) + log := logf.FromContext(ctx) + + crt, err := getCertificateForKey(ctx, key, c.certificateLister) + if apierrors.IsNotFound(err) { + log.Error(err, "certificate resource not found for key", "key", key) + return nil + } + if crt == nil { + log.Info("certificate resource not found for key", "key", key) + return nil + } + if err != nil { + return err + } + + log = logf.WithResource(log, crt) + + if crt.Spec.IssuerRef.Group == "" { + log.V(logf.DebugLevel).Info("certificate issuerRef.group is not set, skipping processing") + return nil + } + + ctx = logf.NewContext(ctx, log) + updatedCert := crt.DeepCopy() + err = c.processCertificate(ctx, updatedCert) + log.V(logf.DebugLevel).Info("check if certificate status update is required") + updateStatusErr := c.updateCertificateStatus(ctx, crt, updatedCert) + // TODO: combine errors + if err != nil { + return err + } + if updateStatusErr != nil { + return err + } + + return nil +} + +func (c *certificateRequestManager) updateCertificateStatus(ctx context.Context, old, crt *cmapi.Certificate) error { + log := logf.FromContext(ctx) + secretExists := true + certs, key, err := kube.SecretTLSKeyPair(ctx, c.secretLister, crt.Namespace, crt.Spec.SecretName) + if err != nil { + if !apierrors.IsNotFound(err) && !errors.IsInvalidData(err) { + return err + } + + if apierrors.IsNotFound(err) { + secretExists = false + } + } + reqs, err := findCertificateRequestsForCertificate(log, crt, c.certificateRequestLister) + if err != nil { + return err + } + var req *cmapi.CertificateRequest + if len(reqs) == 1 { + req = reqs[0] + } + var cert *x509.Certificate + var certExpired bool + if len(certs) > 0 { + cert = certs[0] + certExpired = cert.NotAfter.Before(c.clock.Now()) + } + + var matches bool + var matchErrs []string + if key != nil && cert != nil { + matches, matchErrs = certificateMatchesSpec(crt, key, cert, c.secretLister) + } + + isTempCert := isTemporaryCertificate(cert) + + // begin setting certificate status fields + if !matches || isTempCert { + crt.Status.NotAfter = nil + } else { + metaNotAfter := metav1.NewTime(cert.NotAfter) + crt.Status.NotAfter = &metaNotAfter + } + + // Derive & set 'Ready' condition on Certificate resource + ready := cmapi.ConditionFalse + reason := "" + message := "" + switch { + case !secretExists || key == nil: + reason = "NotFound" + message = "Certificate does not exist" + case matches && !isTempCert && !certExpired: + ready = cmapi.ConditionTrue + reason = "Ready" + message = "Certificate is up to date and has not expired" + case req != nil: + reason = "InProgress" + message = fmt.Sprintf("Waiting for CertificateRequest %q to complete", req.Name) + case cert == nil: + reason = "Pending" + message = "Certificate pending issuance" + case !matches: + reason = "DoesNotMatch" + message = strings.Join(matchErrs, ", ") + case certExpired: + reason = "Expired" + message = fmt.Sprintf("Certificate has expired on %s", cert.NotAfter.Format(time.RFC822)) + case isTempCert: + reason = "TemporaryCertificate" + message = "Certificate issuance in progress. Temporary certificate issued." + default: + // theoretically, it should not be possible to reach this state. + // practically, we may have missed some edge cases above. + // print a dump of the current state as a log message so that users can + // discover, share and attempt to resolve bugs in this area of code easily. + log.Info("unknown certificate state", + "secret_exists", secretExists, + "matches", matches, + "is_temp_cert", isTempCert, + "cert_expired", certExpired, + "key_is_nil", key == nil, + "req_is_nil", req == nil, + "cert_is_nil", cert == nil, + ) + ready = cmapi.ConditionFalse + reason = "Unknown" + message = "Unknown certificate status. Please open an issue and share your controller logs." + } + apiutil.SetCertificateCondition(crt, cmapi.CertificateConditionReady, ready, reason, message) + + _, err = updateCertificateStatus(ctx, metrics.Default, c.cmClient, old, crt) + if err != nil { + return err + } + + return nil +} + +// processCertificate is the core method that is called in the manager. +// It accepts a Certificate resource, and checks to see if the certificate +// requires re-issuance. +func (c *certificateRequestManager) processCertificate(ctx context.Context, crt *cmapi.Certificate) error { + log := logf.FromContext(ctx, ExperimentalControllerName) + dbg := log.V(logf.DebugLevel) + + // The certificate request name is a product of the certificate's spec, + // which makes it unique and predictable. + // First we compute what we expect it to be. + expectedReqName, err := expectedCertificateRequestName(crt) + if err != nil { + return fmt.Errorf("internal error hashing certificate spec: %v", err) + } + + // Clean up any 'owned' CertificateRequest resources that do not have the + // expected name computed above + err = c.cleanupExistingCertificateRequests(log, crt, expectedReqName) + if err != nil { + return err + } + + // Fetch a copy of the existing Secret resource + existingSecret, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if apierrors.IsNotFound(err) { + // If the secret does not exist, generate a new private key and store it. + dbg.Info("existing secret not found, generating and storing private key") + return c.generateAndStorePrivateKey(ctx, crt, nil, c.kubeClient.CoreV1().Secrets(crt.Namespace).Create) + } + if err != nil { + return err + } + + log = logf.WithRelatedResource(log, existingSecret) + ctx = logf.NewContext(ctx, log) + + // If the Secret does not contain a private key, generate one and update + // the Secret resource + existingKey := existingSecret.Data[corev1.TLSPrivateKeyKey] + if len(existingKey) == 0 { + log.Info("existing private key not found in Secret, generate a new private key") + return c.generateAndStorePrivateKey(ctx, crt, existingSecret, c.kubeClient.CoreV1().Secrets(crt.Namespace).Update) + } + + // Ensure the the private key has the correct key algorithm and key size. + dbg.Info("validating private key has correct keyAlgorithm/keySize") + validKey, err := validatePrivateKeyUpToDate(log, existingKey, crt) + // If tls.key contains invalid data, we regenerate a new private key + if errors.IsInvalidData(err) { + log.Info("existing private key data is invalid, generating a new private key") + return c.generateAndStorePrivateKey(ctx, crt, existingSecret, c.kubeClient.CoreV1().Secrets(crt.Namespace).Update) + } + if err != nil { + return err + } + // If the private key is not 'up to date', we generate a new private key + if !validKey { + log.Info("existing private key does not match requirements specified on Certificate resource, generating new private key") + return c.generateAndStorePrivateKey(ctx, crt, existingSecret, c.kubeClient.CoreV1().Secrets(crt.Namespace).Update) + } + + // Attempt to fetch the CertificateRequest with the expected name computed above. + dbg.Info("checking for existing CertificateRequest for Certificate") + existingReq, err := c.certificateRequestLister.CertificateRequests(crt.Namespace).Get(expectedReqName) + // Allow IsNotFound errors, later on we check if existingReq == nil and if + // it is, we create a new CertificateRequest resource. + if err != nil && !apierrors.IsNotFound(err) { + return err + } + if existingReq != nil { + dbg.Info("found existing certificate request for Certificate", "request_name", existingReq.Name) + log = logf.WithRelatedResource(log, existingReq) + } + + needsIssue := true + // Parse the existing certificate + existingCert := existingSecret.Data[corev1.TLSCertKey] + if len(existingCert) > 0 { + // Here we check to see if the existing certificate 'matches' the spec + // of the Certificate resource. + // This includes checking if dnsNames, commonName, organization etc. + // are up to date, as well as validating that the stored private key is + // a valid partner to the stored certificate. + var matchErrs []string + dbg.Info("checking if existing certificate stored in Secret resource is not expiring soon and matches certificate spec") + needsIssue, matchErrs, err = c.certificateRequiresIssuance(ctx, crt, existingKey, existingCert) + if err != nil && !errors.IsInvalidData(err) { + return err + } + // If the certificate data is invalid, we require a re-issuance. + // The private key should never be invalid at this point as we already + // check it above. + if errors.IsInvalidData(err) { + dbg.Info("existing secret contains invalid certificate data") + needsIssue = true + } + + if !needsIssue { + dbg.Info("existing certificate does not need re-issuance") + } else { + dbg.Info("will attempt to issue certificate", "reason", matchErrs) + } + } + + // Exit early if the certificate doesn't need issuing to save extra work + if !needsIssue { + if existingReq != nil { + dbg.Info("skipping issuing certificate data into Secret resource as existing issued certificate is still valid") + } + + // Before exiting, ensure that the Secret resource's metadata is up to + // date. If it isn't, it will be updated. + updated, err := c.ensureSecretMetadataUpToDate(ctx, existingSecret, crt) + if err != nil { + return err + } + + if updated { + log.Info("updated Secret resource metadata as it was out of date") + } + + // As the Certificate has been validated as Ready, schedule a renewal + // for near the expiry date. + scheduleRenewal(ctx, c.secretLister, c.calculateDurationUntilRenew, c.scheduledWorkQueue.Add, crt) + + log.Info("certificate does not require re-issuance. certificate renewal scheduled near expiry time.") + + return nil + } + + // Attempt to decode the private key. + // This shouldn't fail as we already validate the private key is valid above. + dbg.Info("decoding existing private key") + privateKey, err := pki.DecodePrivateKeyBytes(existingKey) + if err != nil { + return err + } + + // Attempt to decode the existing certificate. + // We tolerate invalid data errors as we will issue a certificate if the + // data is invalid. + dbg.Info("attempting to decode existing certificate") + existingX509Cert, err := pki.DecodeX509CertificateBytes(existingCert) + if err != nil && !errors.IsInvalidData(err) { + return err + } + if errors.IsInvalidData(err) { + dbg.Info("existing certificate data is invalid, continuing...") + } + + // Handling for 'temporary certificates' + if c.issueTemporaryCerts { + // Issue a temporary certificate if the current certificate is empty or the + // private key is not valid for the current certificate. + if existingX509Cert == nil { + log.Info("no existing certificate data found in secret, issuing temporary certificate") + return c.issueTemporaryCertificate(ctx, existingSecret, crt, existingKey) + } + // We don't issue a temporary certificate if the existing stored + // certificate already 'matches', even if it isn't a temporary certificate. + matches, _ := certificateMatchesSpec(crt, privateKey, existingX509Cert, c.secretLister) + if !matches { + log.Info("existing certificate fields do not match certificate spec, issuing temporary certificate") + return c.issueTemporaryCertificate(ctx, existingSecret, crt, existingKey) + } + + log.Info("not issuing temporary certificate as existing certificate matches requirements") + + // Ensure the secret metadata is up to date + updated, err := c.ensureSecretMetadataUpToDate(ctx, existingSecret, crt) + if err != nil { + return err + } + + // Only return early if an update actually occurred, otherwise continue. + if updated { + log.Info("updated Secret resource metadata as it was out of date") + return nil + } + } + + if existingReq == nil { + // If no existing CertificateRequest resource exists, we must create one + log.Info("no existing CertificateRequest resource exists, creating new request...") + req, err := c.buildCertificateRequest(log, crt, expectedReqName, existingKey) + if err != nil { + return err + } + + req, err = c.cmClient.CertmanagerV1alpha1().CertificateRequests(crt.Namespace).Create(req) + if err != nil { + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, "Requested", "Created new CertificateRequest resource %q", req.Name) + log.Info("created certificate request", "request_name", req.Name) + + return nil + } + + // Validate the CertificateRequest's CSR is valid + log.Info("validating existing CSR data") + x509CSR, err := pki.DecodeX509CertificateRequestBytes(existingReq.Spec.CSRPEM) + // TODO: handle InvalidData + if err != nil { + return err + } + + // Ensure the stored private key is a 'pair' to the CSR + publicKeyMatches, err := pki.PublicKeyMatchesCSR(privateKey.Public(), x509CSR) + if err != nil { + return err + } + + // if the stored private key does not pair with the CSR on the + // CertificateRequest resource, delete the resource as we won't be able to + // do anything with the certificate if it is issued + if !publicKeyMatches { + log.Info("stored private key is not valid for CSR stored on existing CertificateRequest, recreating CertificateRequest resource") + err := c.cmClient.CertmanagerV1alpha1().CertificateRequests(existingReq.Namespace).Delete(existingReq.Name, nil) + if err != nil { + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, "PrivateKeyLost", "Lost private key for CertificateRequest %q, deleting old resource", existingReq.Name) + log.Info("deleted existing CertificateRequest as the stored private key does not match the CSR") + return nil + } + + // Check if the CertificateRequest is Ready, if it is not then we return + // and wait for informer updates to re-trigger processing. + if !apiutil.CertificateRequestHasCondition(existingReq, cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionReady, + Status: cmapi.ConditionTrue, + }) { + log.Info("certificate request is not in a Ready state, waiting until CertificateRequest is issued") + // TODO: we need to handle failure states too once we have defined how we represent them + return nil + } + + log.Info("CertificateRequest is in a Ready state, issuing certificate...") + + // Decode the certificate bytes so we can ensure the certificate is valid + log.Info("decoding certificate data") + x509Cert, err := pki.DecodeX509CertificateBytes(existingReq.Status.Certificate) + if err != nil { + return err + } + + // Check if the Certificate requires renewal according to the renewBefore + // specified on the Certificate resource. + log.Info("checking if certificate stored on CertificateRequest is up to date") + if c.certificateNeedsRenew(ctx, x509Cert, crt) { + log.Info("certificate stored on CertificateRequest needs renewal, so deleting the old CertificateRequest resource") + err := c.cmClient.CertmanagerV1alpha1().CertificateRequests(existingReq.Namespace).Delete(existingReq.Name, nil) + if err != nil { + return err + } + + return nil + } + + // If certificate stored on CertificateRequest is not expiring soon, copy + // across the status.certificate field into the Secret resource. + log.Info("CertificateRequest contains a valid certificate for issuance. Issuing certificate...") + + _, err = c.updateSecretData(ctx, crt, existingSecret, secretData{pk: existingKey, cert: existingReq.Status.Certificate, ca: existingReq.Status.CA}) + if err != nil { + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, "Issued", "Certificate issued successfully") + + return nil +} + +// updateSecretData will ensure the Secret resource contains the given secret +// data as well as appropriate metadata. +// If the given 'existingSecret' is nil, a new Secret resource will be created. +// Otherwise, the existing resource will be updated. +// The first return argument will be true if the resource was updated/created +// without error. +func (c *certificateRequestManager) updateSecretData(ctx context.Context, crt *cmapi.Certificate, existingSecret *corev1.Secret, data secretData) (bool, error) { + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: crt.Spec.SecretName, + Namespace: crt.Namespace, + }, + Type: corev1.SecretTypeTLS, + } + if existingSecret != nil { + s = existingSecret + } + + newSecret := s.DeepCopy() + err := setSecretValues(ctx, crt, newSecret, secretData{pk: data.pk, cert: data.cert, ca: data.ca}) + if err != nil { + return false, err + } + if reflect.DeepEqual(s, newSecret) { + return false, nil + } + + if existingSecret == nil { + _, err = c.kubeClient.CoreV1().Secrets(newSecret.Namespace).Create(newSecret) + if err != nil { + return false, err + } + return true, nil + } + + _, err = c.kubeClient.CoreV1().Secrets(newSecret.Namespace).Update(newSecret) + if err != nil { + return false, err + } + + return true, nil +} + +func (c *certificateRequestManager) ensureSecretMetadataUpToDate(ctx context.Context, s *corev1.Secret, crt *cmapi.Certificate) (bool, error) { + pk := s.Data[corev1.TLSPrivateKeyKey] + cert := s.Data[corev1.TLSCertKey] + ca := s.Data[TLSCAKey] + + updated, err := c.updateSecretData(ctx, crt, s, secretData{pk: pk, cert: cert, ca: ca}) + if err != nil || !updated { + return updated, err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, "UpdateMeta", "Updated metadata on Secret resource") + + return true, nil +} + +func (c *certificateRequestManager) issueTemporaryCertificate(ctx context.Context, secret *corev1.Secret, crt *cmapi.Certificate, key []byte) error { + tempCertData, err := c.localTemporarySigner(crt, key) + if err != nil { + return err + } + + newSecret := secret.DeepCopy() + err = setSecretValues(ctx, crt, newSecret, secretData{pk: key, cert: tempCertData}) + if err != nil { + return err + } + + newSecret, err = c.kubeClient.CoreV1().Secrets(newSecret.Namespace).Update(newSecret) + if err != nil { + return err + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, "TempCert", "Issued temporary certificate") + + return nil +} + +func (c *certificateRequestManager) certificateRequiresIssuance(ctx context.Context, crt *cmapi.Certificate, keyBytes, certBytes []byte) (bool, []string, error) { + key, err := pki.DecodePrivateKeyBytes(keyBytes) + if err != nil { + return false, nil, err + } + cert, err := pki.DecodeX509CertificateBytes(certBytes) + if err != nil { + return false, nil, err + } + if isTemporaryCertificate(cert) { + return true, nil, nil + } + matches, matchErrs := certificateMatchesSpec(crt, key, cert, c.secretLister) + if !matches { + return true, matchErrs, nil + } + needsRenew := c.certificateNeedsRenew(ctx, cert, crt) + return needsRenew, []string{"Certificate is expiring soon"}, nil +} + +func expectedCertificateRequestName(crt *cmapi.Certificate) (string, error) { + crt = crt.DeepCopy() + // clear deprecated ACME field as it is not supported with CertificateRequest + crt.Spec.ACME = nil + specBytes, err := json.Marshal(crt.Spec) + if err != nil { + return "", err + } + + hashF := fnv.New32() + _, err = hashF.Write(specBytes) + if err != nil { + return "", err + } + + // shorten the cert name to 52 chars to ensure the total length of the name + // is less than or equal to 64 characters + return fmt.Sprintf("%.52s-%d", crt.Name, hashF.Sum32()), nil +} + +type generateCSRFn func(*cmapi.Certificate, []byte) ([]byte, error) + +func generateCSRImpl(crt *cmapi.Certificate, pk []byte) ([]byte, error) { + csr, err := pki.GenerateCSR(crt) + if err != nil { + return nil, err + } + + signer, err := pki.DecodePrivateKeyBytes(pk) + if err != nil { + return nil, err + } + + csrDER, err := pki.EncodeCSR(csr, signer) + if err != nil { + return nil, err + } + + csrPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", Bytes: csrDER, + }) + + return csrPEM, nil +} + +func (c *certificateRequestManager) buildCertificateRequest(log logr.Logger, crt *cmapi.Certificate, name string, pk []byte) (*cmapi.CertificateRequest, error) { + csrPEM, err := c.generateCSR(crt, pk) + if err != nil { + return nil, err + } + + return &cmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: crt.Namespace, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: cmapi.CertificateRequestSpec{ + CSRPEM: csrPEM, + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + IsCA: crt.Spec.IsCA, + }, + }, nil +} + +func (c *certificateRequestManager) cleanupExistingCertificateRequests(log logr.Logger, crt *cmapi.Certificate, retain string) error { + reqs, err := findCertificateRequestsForCertificate(log, crt, c.certificateRequestLister) + if err != nil { + return err + } + + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if req.Name == retain { + log.V(logf.DebugLevel).Info("skipping deleting CertificateRequest as it is up to date for the certificate spec") + continue + } + + err = c.cmClient.CertmanagerV1alpha1().CertificateRequests(req.Namespace).Delete(req.Name, nil) + if err != nil { + return err + } + + log.Info("deleted no longer required CertificateRequest") + } + + return nil +} + +func findCertificateRequestsForCertificate(log logr.Logger, crt *cmapi.Certificate, lister cmlisters.CertificateRequestLister) ([]*cmapi.CertificateRequest, error) { + log.V(logf.DebugLevel).Info("finding existing CertificateRequest resources for Certificate") + reqs, err := lister.CertificateRequests(crt.Namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var candidates []*cmapi.CertificateRequest + for _, req := range reqs { + log := logf.WithRelatedResource(log, req) + if metav1.IsControlledBy(req, crt) { + log.V(logf.DebugLevel).Info("found CertificateRequest resource for Certificate") + candidates = append(candidates, &(*req)) + } + } + + return candidates, nil +} + +// validatePrivateKeyUpToDate will evaluate the private key data in pk and +// ensure it is 'up to date' and matches the specification of the key as +// required by the given Certificate resource. +// It returns false if the private key isn't up to date, e.g. the Certificate +// resource specifies a different keyEncoding, keyAlgorithm or keySize. +func validatePrivateKeyUpToDate(log logr.Logger, pk []byte, crt *cmapi.Certificate) (bool, error) { + signer, err := pki.DecodePrivateKeyBytes(pk) + if err != nil { + return false, err + } + + // TODO: check keyEncoding + + wantedAlgorithm := crt.Spec.KeyAlgorithm + if wantedAlgorithm == "" { + // in-memory defaulting of the key algorithm to RSA + // TODO: remove this in favour of actual defaulting in a mutating webhook + wantedAlgorithm = cmapi.RSAKeyAlgorithm + } + + switch wantedAlgorithm { + case cmapi.RSAKeyAlgorithm: + _, ok := signer.(*rsa.PrivateKey) + if !ok { + log.Info("expected private key's algorithm to be RSA but it is not") + return false, nil + } + // TODO: check keySize + case cmapi.ECDSAKeyAlgorithm: + _, ok := signer.(*ecdsa.PrivateKey) + if !ok { + log.Info("expected private key's algorithm to be ECDSA but it is not") + return false, nil + } + // TODO: check keySize + } + + return true, nil +} + +type secretSaveFn func(*corev1.Secret) (*corev1.Secret, error) + +func (c *certificateRequestManager) generateAndStorePrivateKey(ctx context.Context, crt *cmapi.Certificate, s *corev1.Secret, saveFn secretSaveFn) error { + keyData, err := c.generatePrivateKeyBytes(ctx, crt) + if err != nil { + // TODO: handle permanent failures caused by invalid spec + return err + } + + updated, err := c.updateSecretData(ctx, crt, s, secretData{pk: keyData}) + if err != nil { + return err + } + if !updated { + return nil + } + + c.recorder.Eventf(crt, corev1.EventTypeNormal, "GeneratedKey", "Generated a new private key") + + return nil +} + +type generatePrivateKeyBytesFn func(context.Context, *cmapi.Certificate) ([]byte, error) + +func generatePrivateKeyBytesImpl(ctx context.Context, crt *cmapi.Certificate) ([]byte, error) { + signer, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return nil, err + } + + keyData, err := pki.EncodePrivateKey(signer, crt.Spec.KeyEncoding) + if err != nil { + return nil, err + } + + return keyData, nil +} + +// secretData is a structure wrapping private key, certificate and CA data +type secretData struct { + pk, cert, ca []byte +} + +// setSecretValues will update the Secret resource 's' with the data contained +// in the given secretData. +// It will update labels and annotations on the Secret resource appropriately. +// The Secret resource 's' must be non-nil, although may be a resource that does +// not exist in the Kubernetes apiserver yet. +// setSecretValues will NOT actually update the resource in the apiserver. +// If updating an existing Secret resource returned by an api client 'lister', +// make sure to DeepCopy the object first to avoid modifying data in-cache. +func setSecretValues(ctx context.Context, crt *cmapi.Certificate, s *corev1.Secret, data secretData) error { + // initialize the `Data` field if it is nil + if s.Data == nil { + s.Data = make(map[string][]byte) + } + + s.Data[corev1.TLSPrivateKeyKey] = data.pk + s.Data[corev1.TLSCertKey] = data.cert + s.Data[TLSCAKey] = data.ca + + if s.Annotations == nil { + s.Annotations = make(map[string]string) + } + + s.Annotations[cmapi.CertificateNameKey] = crt.Name + s.Annotations[cmapi.IssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name + s.Annotations[cmapi.IssuerKindAnnotationKey] = issuerKind(crt.Spec.IssuerRef) + + // if the certificate data is empty, clear the subject related annotations + if len(data.cert) == 0 { + delete(s.Annotations, cmapi.CommonNameAnnotationKey) + delete(s.Annotations, cmapi.AltNamesAnnotationKey) + delete(s.Annotations, cmapi.IPSANAnnotationKey) + } else { + x509Cert, err := pki.DecodeX509CertificateBytes(data.cert) + // TODO: handle InvalidData here? + if err != nil { + return err + } + + s.Annotations[cmapi.CommonNameAnnotationKey] = x509Cert.Subject.CommonName + s.Annotations[cmapi.AltNamesAnnotationKey] = strings.Join(x509Cert.DNSNames, ",") + s.Annotations[cmapi.IPSANAnnotationKey] = strings.Join(pki.IPAddressesToString(x509Cert.IPAddresses), ",") + } + + return nil +} + +const ( + ExperimentalControllerName = "certificates-experimental" +) + +func init() { + controllerpkg.Register(ExperimentalControllerName, func(ctx *controllerpkg.Context) (controllerpkg.Interface, error) { + c, err := controllerpkg.New(ctx, ExperimentalControllerName, &certificateRequestManager{}) + if err != nil { + return nil, err + } + return c.Run, nil + }) +} diff --git a/pkg/controller/certificates/certificate_request_test.go b/pkg/controller/certificates/certificate_request_test.go new file mode 100644 index 000000000..7338f4c7a --- /dev/null +++ b/pkg/controller/certificates/certificate_request_test.go @@ -0,0 +1,1957 @@ +/* +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 certificates + +import ( + "context" + "crypto" + "crypto/x509" + "fmt" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + 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" + + apiutil "github.com/jetstack/cert-manager/pkg/api/util" + 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/util/pki" + "github.com/jetstack/cert-manager/test/unit/gen" +) + +var ( + fixedClockStart = time.Now() + fixedClock = fakeclock.NewFakeClock(fixedClockStart) +) + +type cryptoBundle struct { + // certificate is the Certificate resource used to create this bundle + certificate *cmapi.Certificate + // expectedRequestName is the name of the CertificateRequest that is + // expected to be created to issue this certificate + expectedRequestName string + + // privateKey is the private key used as the complement to the certificates + // in this bundle + privateKey crypto.Signer + privateKeyBytes []byte + + // csr is the CSR used to obtain the certificate in this bundle + csr *x509.CertificateRequest + csrBytes []byte + + // certificateRequest is the request that is expected to be created to + // obtain a certificate when using this bundle + certificateRequest *cmapi.CertificateRequest + certificateRequestReady *cmapi.CertificateRequest + + // cert is a signed certificate + cert *x509.Certificate + certBytes []byte + + localTemporaryCertificateBytes []byte +} + +func mustCreateCryptoBundle(t *testing.T, crt *cmapi.Certificate) cryptoBundle { + c, err := createCryptoBundle(crt) + if err != nil { + t.Fatalf("error generating crypto bundle: %v", err) + } + return *c +} + +func createCryptoBundle(crt *cmapi.Certificate) (*cryptoBundle, error) { + reqName, err := expectedCertificateRequestName(crt) + if err != nil { + return nil, err + } + + privateKey, err := pki.GeneratePrivateKeyForCertificate(crt) + if err != nil { + return nil, err + } + + privateKeyBytes, err := pki.EncodePrivateKey(privateKey, crt.Spec.KeyEncoding) + if err != nil { + return nil, err + } + + csrPEM, err := generateCSRImpl(crt, privateKeyBytes) + if err != nil { + return nil, err + } + + csr, err := pki.DecodeX509CertificateRequestBytes(csrPEM) + if err != nil { + return nil, err + } + + certificateRequest := &cmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: reqName, + Namespace: crt.Namespace, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}, + }, + Spec: cmapi.CertificateRequestSpec{ + CSRPEM: csrPEM, + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + IsCA: crt.Spec.IsCA, + }, + } + + unsignedCert, err := pki.GenerateTemplateFromCertificateRequest(certificateRequest) + if err != nil { + return nil, err + } + + certBytes, cert, err := pki.SignCertificate(unsignedCert, unsignedCert, privateKey.Public(), privateKey) + if err != nil { + return nil, err + } + + certificateRequestReady := gen.CertificateRequestFrom(certificateRequest, + gen.SetCertificateRequestCertificate(certBytes), + gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionReady, + Status: cmapi.ConditionTrue, + }), + ) + + tempCertBytes, err := generateLocallySignedTemporaryCertificate(crt, privateKeyBytes) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + return &cryptoBundle{ + certificate: crt, + expectedRequestName: reqName, + privateKey: privateKey, + privateKeyBytes: privateKeyBytes, + csr: csr, + csrBytes: csrPEM, + certificateRequest: certificateRequest, + certificateRequestReady: certificateRequestReady, + cert: cert, + certBytes: certBytes, + localTemporaryCertificateBytes: tempCertBytes, + }, nil +} + +func (c *cryptoBundle) generateTestCSR(crt *cmapi.Certificate) []byte { + csrPEM, err := generateCSRImpl(crt, c.privateKeyBytes) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + return csrPEM +} + +func (c *cryptoBundle) generateTestCertificate(crt *cmapi.Certificate, notBefore *time.Time) []byte { + csr := c.generateTestCSR(crt) + certificateRequest := &cmapi.CertificateRequest{ + Spec: cmapi.CertificateRequestSpec{ + CSRPEM: csr, + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + IsCA: crt.Spec.IsCA, + }, + } + + unsignedCert, err := pki.GenerateTemplateFromCertificateRequest(certificateRequest) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + if notBefore != nil { + unsignedCert.NotBefore = *notBefore + } + + certBytes, _, err := pki.SignCertificate(unsignedCert, unsignedCert, c.privateKey.Public(), c.privateKey) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + return certBytes +} + +func (c *cryptoBundle) generateCertificateExpiring1H(crt *cmapi.Certificate) []byte { + csr := c.generateTestCSR(crt) + certificateRequest := &cmapi.CertificateRequest{ + Spec: cmapi.CertificateRequestSpec{ + CSRPEM: csr, + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + IsCA: crt.Spec.IsCA, + }, + } + + unsignedCert, err := pki.GenerateTemplateFromCertificateRequest(certificateRequest) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + nowTime := fixedClock.Now() + duration := unsignedCert.NotAfter.Sub(unsignedCert.NotBefore) + unsignedCert.NotBefore = nowTime.Add(time.Hour).Add(-1 * duration) + unsignedCert.NotAfter = nowTime.Add(time.Hour) + + certBytes, _, err := pki.SignCertificate(unsignedCert, unsignedCert, c.privateKey.Public(), c.privateKey) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + return certBytes +} + +func (c *cryptoBundle) generateCertificateExpired(crt *cmapi.Certificate) []byte { + csr := c.generateTestCSR(crt) + certificateRequest := &cmapi.CertificateRequest{ + Spec: cmapi.CertificateRequestSpec{ + CSRPEM: csr, + Duration: crt.Spec.Duration, + IssuerRef: crt.Spec.IssuerRef, + IsCA: crt.Spec.IsCA, + }, + } + + unsignedCert, err := pki.GenerateTemplateFromCertificateRequest(certificateRequest) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + nowTime := fixedClock.Now() + duration := unsignedCert.NotAfter.Sub(unsignedCert.NotBefore) + unsignedCert.NotBefore = nowTime.Add(-1 * time.Hour).Add(-1 * duration) + unsignedCert.NotAfter = nowTime.Add(-1 * time.Hour) + + certBytes, _, err := pki.SignCertificate(unsignedCert, unsignedCert, c.privateKey.Public(), c.privateKey) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + + return certBytes +} + +func (c *cryptoBundle) generateCertificateTemporary(crt *cmapi.Certificate) []byte { + d, err := generateLocallySignedTemporaryCertificate(crt, c.privateKeyBytes) + if err != nil { + panic("failed to generate test fixture: " + err.Error()) + } + return d +} + +func certificateNotAfter(b []byte) time.Time { + cert, err := pki.DecodeX509CertificateBytes(b) + if err != nil { + panic("failed to decode certificate: " + err.Error()) + } + return cert.NotAfter +} + +func testGeneratePrivateKeyBytesFn(b []byte) generatePrivateKeyBytesFn { + return func(context.Context, *cmapi.Certificate) ([]byte, error) { + return b, nil + } +} + +func testGenerateCSRFn(b []byte) generateCSRFn { + return func(_ *cmapi.Certificate, _ []byte) ([]byte, error) { + return b, nil + } +} + +func testLocalTemporarySignerFn(b []byte) localTemporarySignerFn { + return func(crt *cmapi.Certificate, pk []byte) ([]byte, error) { + return b, nil + } +} + +func TestProcessCertificate(t *testing.T) { + baseCert := gen.Certificate("test", + gen.SetCertificateIssuer(cmapi.ObjectReference{Name: "test", Kind: "something", Group: "not-empty"}), + gen.SetCertificateSecretName("output"), + gen.SetCertificateRenewBefore(time.Hour*36), + ) + exampleBundle1 := mustCreateCryptoBundle(t, gen.CertificateFrom(baseCert, + gen.SetCertificateDNSNames("example.com"), + )) + exampleECBundle := mustCreateCryptoBundle(t, gen.CertificateFrom(baseCert, + gen.SetCertificateDNSNames("example.com"), + gen.SetCertificateKeyAlgorithm(cmapi.ECDSAKeyAlgorithm), + )) + + tests := map[string]testT{ + "generate a private key and create a new secret if one does not exist": { + certificate: exampleBundle1.certificate, + generatePrivateKeyBytes: testGeneratePrivateKeyBytesFn(exampleBundle1.privateKeyBytes), + builder: &testpkg.Builder{ + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewCreateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "generate a private key and update an existing secret if one already exists": { + certificate: exampleBundle1.certificate, + generatePrivateKeyBytes: testGeneratePrivateKeyBytesFn(exampleBundle1.privateKeyBytes), + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + }, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "generate a new private key and update the Secret if the existing private key data is garbage": { + certificate: exampleBundle1.certificate, + generatePrivateKeyBytes: testGeneratePrivateKeyBytesFn(exampleBundle1.privateKeyBytes), + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + }, + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: []byte("invalid"), + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "generate a new private key and update the Secret if the existing private key data has a differing keyAlgorithm": { + certificate: exampleBundle1.certificate, + generatePrivateKeyBytes: testGeneratePrivateKeyBytesFn(exampleBundle1.privateKeyBytes), + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + }, + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleECBundle.privateKeyBytes, + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "create a new certificatesigningrequest resource if the secret contains a private key but no certificate": { + certificate: exampleBundle1.certificate, + generateCSR: testGenerateCSRFn(exampleBundle1.csrBytes), + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewCreateAction( + cmapi.SchemeGroupVersion.WithResource("certificaterequests"), + gen.DefaultTestNamespace, + exampleBundle1.certificateRequest, + )), + }, + }, + }, + "delete an existing certificaterequest that does not have matching dnsnames": { + certificate: exampleBundle1.certificate, + generateCSR: testGenerateCSRFn(exampleBundle1.csrBytes), + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + gen.CertificateRequestFrom(exampleBundle1.certificateRequest, + gen.SetCertificateRequestName("not-expected-name"), + gen.SetCertificateRequestCSR( + exampleBundle1.generateTestCSR(gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateDNSNames("notexample.com"), + )), + ), + ), + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewDeleteAction( + cmapi.SchemeGroupVersion.WithResource("certificaterequests"), + gen.DefaultTestNamespace, + "not-expected-name", + )), + testpkg.NewAction(coretesting.NewCreateAction( + cmapi.SchemeGroupVersion.WithResource("certificaterequests"), + gen.DefaultTestNamespace, + exampleBundle1.certificateRequest, + )), + }, + }, + }, + "do nothing and wait if an up to date certificaterequest resource exists and is not Ready": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: "Issuer", + cmapi.IssuerNameAnnotationKey: "test", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + }, + }, + "create a new CertificateRequest if existing Certificate expires soon": { + certificate: exampleBundle1.certificate, + generateCSR: testGenerateCSRFn(exampleBundle1.csrBytes), + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.generateCertificateExpiring1H(exampleBundle1.certificate), + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewCreateAction( + cmapi.SchemeGroupVersion.WithResource("certificaterequests"), + gen.DefaultTestNamespace, + exampleBundle1.certificateRequest, + )), + }, + }, + }, + "do nothing if existing x509 certificate is up to date and valid for the cert and no other CertificateRequest exists": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.certBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + }, + }, + "update secret resource metadata if existing certificate is valid but missing annotations": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.certBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.certBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "update the Secret resource with the signed certificate if the CertificateRequest is ready": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequestReady, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.certBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "do nothing if the Secret resource is not in need of issuance even if a Ready CertificateRequest exists and contains different data": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: exampleBundle1.certificate.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.generateTestCertificate(exampleBundle1.certificate, nil), + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequestReady, + }, + }, + }, + "issue new certificate if existing certificate data is garbage": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: []byte("invalid"), + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequestReady, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.certBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "delete existing certificate request if existing one contains a certificate nearing expiry": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.generateCertificateExpiring1H(exampleBundle1.certificate), + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + gen.CertificateRequestFrom(exampleBundle1.certificateRequestReady, + gen.SetCertificateRequestCertificate( + exampleBundle1.generateCertificateExpiring1H(exampleBundle1.certificate), + ), + ), + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewDeleteAction( + cmapi.SchemeGroupVersion.WithResource("certificaterequests"), + gen.DefaultTestNamespace, + exampleBundle1.certificateRequestReady.Name, + )), + }, + }, + }, + "delete existing certificate request if existing one contains a csr not valid for stored private key": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.generateCertificateExpiring1H(exampleBundle1.certificate), + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + gen.CertificateRequestFrom(exampleBundle1.certificateRequestReady, + gen.SetCertificateRequestCSR(exampleECBundle.csrBytes), + ), + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewDeleteAction( + cmapi.SchemeGroupVersion.WithResource("certificaterequests"), + gen.DefaultTestNamespace, + exampleBundle1.certificateRequestReady.Name, + )), + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test.enableTempCerts = false + runTest(t, test) + }) + } +} + +func TestTemporaryCertificateEnabled(t *testing.T) { + baseCert := gen.Certificate("test", + gen.SetCertificateIssuer(cmapi.ObjectReference{Name: "test", Kind: "something", Group: "not-empty"}), + gen.SetCertificateSecretName("output"), + gen.SetCertificateRenewBefore(time.Hour*36), + ) + exampleBundle1 := mustCreateCryptoBundle(t, gen.CertificateFrom(baseCert, + gen.SetCertificateDNSNames("example.com"), + )) + + tests := map[string]testT{ + "issue a temporary certificate if no existing request exists and secret does not contain a cert": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "issue a temporary certificate if existing request is pending and secret does not contain a cert": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "issue a temporary certificate if existing request is Ready and secret does not contain a cert": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: nil, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "update the Secret resource with the signed certificate if the CertificateRequest is ready and contains temporary signed certificate": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequestReady, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.certBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "issue new certificate if existing certificate data is garbage, even if existing CertificateRequest is Ready": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: []byte("invalid"), + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequestReady, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "generate a new temporary certificate if existing one is valid for different dnsNames": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.generateCertificateTemporary( + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateDNSNames("notexample.com"), + ), + ), + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + "update the secret metadata if existing temporary certificate does not have annotations": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + corev1.SchemeGroupVersion.WithResource("secrets"), + gen.DefaultTestNamespace, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: gen.DefaultTestNamespace, + Name: "output", + Annotations: map[string]string{ + "custom-annotation": "value", + cmapi.CertificateNameKey: "test", + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IPSANAnnotationKey: "", + cmapi.AltNamesAnnotationKey: "example.com", + cmapi.CommonNameAnnotationKey: "example.com", + }, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + TLSCAKey: nil, + }, + Type: corev1.SecretTypeTLS, + }, + )), + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test.enableTempCerts = true + test.localTemporarySigner = testLocalTemporarySignerFn(exampleBundle1.localTemporaryCertificateBytes) + runTest(t, test) + }) + } +} + +func TestUpdateStatus(t *testing.T) { + baseCert := gen.Certificate("test", + gen.SetCertificateIssuer(cmapi.ObjectReference{Name: "test", Kind: "something", Group: "not-empty"}), + gen.SetCertificateSecretName("output"), + gen.SetCertificateRenewBefore(time.Hour*36), + ) + exampleBundle1 := mustCreateCryptoBundle(t, gen.CertificateFrom(baseCert, + gen.SetCertificateDNSNames("example.com"), + )) + + metaFixedClockStart := metav1.NewTime(fixedClockStart) + tests := map[string]testT{ + "mark status as NotFound if Secret does not exist for Certificate": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "NotFound", + Message: "Certificate does not exist", + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + "mark status as NotFound if Secret does not contain any data": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + }, + Data: map[string][]byte{}, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "NotFound", + Message: "Certificate does not exist", + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + "mark certificate as pending issuance if a secret exists with only a private key and no request exists": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "Pending", + Message: "Certificate pending issuance", + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + "mark certificate as in progress if existing Secret contains only private key and request exists & is up to date": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "InProgress", + Message: fmt.Sprintf("Waiting for CertificateRequest %q to complete", exampleBundle1.certificateRequest.Name), + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + "mark certificate Ready if existing certificate is valid and up to date": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.certBytes, + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionTrue, + Reason: "Ready", + Message: "Certificate is up to date and has not expired", + LastTransitionTime: &metaFixedClockStart, + }), + gen.SetCertificateNotAfter(metav1.NewTime(exampleBundle1.cert.NotAfter)), + ), + )), + }, + }, + }, + "mark certificate Ready if existing certificate is expiring soon and a pending CertificateRequest exists": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.generateCertificateExpiring1H(exampleBundle1.certificate), + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionTrue, + Reason: "Ready", + Message: "Certificate is up to date and has not expired", + LastTransitionTime: &metaFixedClockStart, + }), + gen.SetCertificateNotAfter(metav1.NewTime(certificateNotAfter(exampleBundle1.generateCertificateExpiring1H(exampleBundle1.certificate)))), + ), + )), + }, + }, + }, + "mark certificate Expired if existing certificate is expired": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.generateCertificateExpired(exampleBundle1.certificate), + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "Expired", + Message: fmt.Sprintf("Certificate has expired on %s", certificateNotAfter(exampleBundle1.generateCertificateExpired(exampleBundle1.certificate)).Format(time.RFC822)), + LastTransitionTime: &metaFixedClockStart, + }), + gen.SetCertificateNotAfter(metav1.NewTime(certificateNotAfter(exampleBundle1.generateCertificateExpired(exampleBundle1.certificate)))), + ), + )), + }, + }, + }, + "mark certificate InProgress if existing certificate is expired and CertificateRequest is in progress": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.generateCertificateExpired(exampleBundle1.certificate), + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "InProgress", + Message: fmt.Sprintf("Waiting for CertificateRequest %q to complete", exampleBundle1.certificateRequest.Name), + LastTransitionTime: &metaFixedClockStart, + }), + gen.SetCertificateNotAfter(metav1.NewTime(certificateNotAfter(exampleBundle1.generateCertificateExpired(exampleBundle1.certificate)))), + ), + )), + }, + }, + }, + "mark certificate InProgress if existing certificate is expired and CertificateRequest is ready but not stored yet": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.generateCertificateExpired(exampleBundle1.certificate), + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequestReady, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "InProgress", + Message: fmt.Sprintf("Waiting for CertificateRequest %q to complete", exampleBundle1.certificateRequest.Name), + LastTransitionTime: &metaFixedClockStart, + }), + gen.SetCertificateNotAfter(metav1.NewTime(certificateNotAfter(exampleBundle1.generateCertificateExpired(exampleBundle1.certificate)))), + ), + )), + }, + }, + }, + "mark certificate DoesNotMatch if existing Certificate does not match spec and no request is in progress": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.generateTestCertificate( + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateDNSNames("notexample.com"), + ), nil, + ), + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "DoesNotMatch", + Message: "Common name on TLS certificate not up to date: \"notexample.com\", DNS names on TLS certificate not up to date: [\"notexample.com\"]", + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + "mark certificate InProgress if existing Certificate does not match spec and a request is in progress": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.generateTestCertificate( + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateDNSNames("notexample.com"), + ), nil, + ), + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "InProgress", + Message: fmt.Sprintf("Waiting for CertificateRequest %q to complete", exampleBundle1.certificateRequest.Name), + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + "mark certificate TemporaryCertificate if secret contains a valid temporary certificate and no request exists": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "TemporaryCertificate", + Message: "Certificate issuance in progress. Temporary certificate issued.", + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + "mark certificate InProgress if secret contains a valid temporary certificate and a request exists": { + certificate: exampleBundle1.certificate, + builder: &testpkg.Builder{ + KubeObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: exampleBundle1.certificate.Spec.SecretName, + Namespace: exampleBundle1.certificate.Namespace, + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Name, + cmapi.IssuerKindAnnotationKey: exampleBundle1.certificate.Spec.IssuerRef.Kind, + }, + }, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: exampleBundle1.privateKeyBytes, + corev1.TLSCertKey: exampleBundle1.localTemporaryCertificateBytes, + }, + }, + }, + CertManagerObjects: []runtime.Object{ + exampleBundle1.certificate, + exampleBundle1.certificateRequest, + }, + ExpectedActions: []testpkg.Action{ + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + gen.DefaultTestNamespace, + gen.CertificateFrom(exampleBundle1.certificate, + gen.SetCertificateStatusCondition(cmapi.CertificateCondition{ + Type: cmapi.CertificateConditionReady, + Status: cmapi.ConditionFalse, + Reason: "InProgress", + Message: fmt.Sprintf("Waiting for CertificateRequest %q to complete", exampleBundle1.certificateRequest.Name), + LastTransitionTime: &metaFixedClockStart, + }), + ), + )), + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test.builder.T = t + test.builder.Start() + defer test.builder.Stop() + fixedClock.SetTime(fixedClockStart) + apiutil.Clock = fixedClock + + testManager := &certificateRequestManager{} + testManager.Register(test.builder.Context) + testManager.generatePrivateKeyBytes = test.generatePrivateKeyBytes + testManager.generateCSR = test.generateCSR + testManager.clock = fixedClock + test.builder.Sync() + + err := testManager.updateCertificateStatus(context.Background(), test.certificate, test.certificate.DeepCopy()) + + if err != nil && !test.expectedErr { + t.Errorf("expected to not get an error, but got: %v", err) + } + if err == nil && test.expectedErr { + t.Errorf("expected to get an error but did not get one") + } + if err := test.builder.AllReactorsCalled(); err != nil { + t.Errorf("Not all expected reactors were called: %v", err) + } + if err := test.builder.AllActionsExecuted(); err != nil { + t.Errorf(err.Error()) + } + }) + } +} + +type testT struct { + builder *testpkg.Builder + generatePrivateKeyBytes generatePrivateKeyBytesFn + generateCSR generateCSRFn + localTemporarySigner localTemporarySignerFn + enableTempCerts bool + certificate *cmapi.Certificate + expectedErr bool +} + +func runTest(t *testing.T, test testT) { + test.builder.T = t + test.builder.Start() + defer test.builder.Stop() + fixedClock.SetTime(fixedClockStart) + apiutil.Clock = fixedClock + + testManager := &certificateRequestManager{issueTemporaryCerts: test.enableTempCerts} + testManager.Register(test.builder.Context) + testManager.generatePrivateKeyBytes = test.generatePrivateKeyBytes + testManager.generateCSR = test.generateCSR + testManager.localTemporarySigner = test.localTemporarySigner + testManager.issueTemporaryCerts = test.enableTempCerts + testManager.clock = fixedClock + test.builder.Sync() + + err := testManager.processCertificate(context.Background(), test.certificate) + + if err != nil && !test.expectedErr { + t.Errorf("expected to not get an error, but got: %v", err) + } + if err == nil && test.expectedErr { + t.Errorf("expected to get an error but did not get one") + } + if err := test.builder.AllReactorsCalled(); err != nil { + t.Errorf("Not all expected reactors were called: %v", err) + } + if err := test.builder.AllActionsExecuted(); err != nil { + t.Errorf(err.Error()) + } +} diff --git a/pkg/controller/certificates/checks.go b/pkg/controller/certificates/checks.go index cf063aad1..2c2d78711 100644 --- a/pkg/controller/certificates/checks.go +++ b/pkg/controller/certificates/checks.go @@ -19,10 +19,14 @@ package certificates import ( "fmt" - cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" - logf "github.com/jetstack/cert-manager/pkg/logs" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/util/workqueue" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha1" + logf "github.com/jetstack/cert-manager/pkg/logs" ) func (c *controller) handleGenericIssuer(obj interface{}) { @@ -51,34 +55,36 @@ func (c *controller) handleGenericIssuer(obj interface{}) { } } -func (c *controller) handleSecretResource(obj interface{}) { - log := c.log.WithName("handleSecretResource") +func secretResourceHandler(log logr.Logger, certificateLister cmlisters.CertificateLister, queue workqueue.Interface) func(obj interface{}) { + return func(obj interface{}) { + log := log.WithName("handleSecretResource") - secret, ok := obj.(*corev1.Secret) - if !ok { - log.Error(nil, "object is not a Secret resource") - return - } - log = logf.WithResource(log, secret) - - crts, err := c.certificatesForSecret(secret) - if err != nil { - log.Error(err, "error looking up Certificates observing Secret") - return - } - for _, crt := range crts { - log := logf.WithRelatedResource(log, crt) - key, err := keyFunc(crt) - if err != nil { - log.Error(err, "error computing key for resource") - continue + secret, ok := obj.(*corev1.Secret) + if !ok { + log.Error(nil, "object is not a Secret resource") + return + } + log = logf.WithResource(log, secret) + + crts, err := certificatesForSecret(certificateLister, secret) + if err != nil { + log.Error(err, "error looking up Certificates observing Secret") + return + } + for _, crt := range crts { + log := logf.WithRelatedResource(log, crt) + key, err := keyFunc(crt) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + queue.Add(key) } - c.queue.Add(key) } } -func (c *controller) certificatesForSecret(secret *corev1.Secret) ([]*cmapi.Certificate, error) { - crts, err := c.certificateLister.List(labels.NewSelector()) +func certificatesForSecret(certificateLister cmlisters.CertificateLister, secret *corev1.Secret) ([]*cmapi.Certificate, error) { + crts, err := certificateLister.List(labels.NewSelector()) if err != nil { return nil, fmt.Errorf("error listing certificiates: %s", err.Error()) diff --git a/pkg/controller/certificates/controller.go b/pkg/controller/certificates/controller.go index 13b4bec4f..aa0b5ee9a 100644 --- a/pkg/controller/certificates/controller.go +++ b/pkg/controller/certificates/controller.go @@ -58,6 +58,7 @@ type controller struct { // used for testing clock clock.Clock + // used to record Events about resources to the API recorder record.EventRecorder @@ -82,13 +83,15 @@ type controller struct { // and certificate spec. // This is a field on the controller struct to avoid having to maintain a reference // to the controller context, and to make it easier to fake out this call during tests. - calculateDurationUntilRenew func(ctx context.Context, cert *x509.Certificate, crt *v1alpha1.Certificate) time.Duration + calculateDurationUntilRenew calculateDurationUntilRenewFn // if addOwnerReferences is enabled then the controller will add owner references // to the secret resources it creates addOwnerReferences bool } +type calculateDurationUntilRenewFn func(context.Context, *x509.Certificate, *v1alpha1.Certificate) time.Duration + // Register registers and constructs the controller using the provided context. // It returns the workqueue to be used to enqueue items, a list of // InformerSynced functions that must be synced, or an error. @@ -133,9 +136,9 @@ func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitin // register handler functions certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) - secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleSecretResource}) + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: secretResourceHandler(c.log, c.certificateLister, c.queue)}) ordersInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ - WorkFunc: controllerpkg.HandleOwnedResourceNamespacedFunc(c.log, c.queue, certificateGvk, c.certificateGetter), + WorkFunc: controllerpkg.HandleOwnedResourceNamespacedFunc(c.log, c.queue, certificateGvk, certificateGetter(c.certificateLister)), }) // Create a scheduled work queue that calls the ctrl.queue.Add method for @@ -173,30 +176,48 @@ func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitin } func (c *controller) ProcessItem(ctx context.Context, key string) error { + ctx = logf.NewContext(ctx, nil, ControllerName) log := logf.FromContext(ctx) - namespace, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - log.Error(err, "invalid resource key") + + crt, err := getCertificateForKey(ctx, key, c.certificateLister) + if k8sErrors.IsNotFound(err) { + log.Error(err, "certificate resource not found for key", "key", key) + return nil + } + if crt == nil { + log.Info("certificate resource not found for key", "key", key) return nil } - - crt, err := c.certificateLister.Certificates(namespace).Get(name) if err != nil { - if k8sErrors.IsNotFound(err) { - c.scheduledWorkQueue.Forget(key) - log.Error(err, "certificate in work queue no longer exists") - return nil - } - return err } - ctx = logf.NewContext(ctx, logf.WithResource(log, crt)) return c.Sync(ctx, crt) } -func (c *controller) certificateGetter(namespace, name string) (interface{}, error) { - return c.certificateLister.Certificates(namespace).Get(name) +type syncFn func(context.Context, *v1alpha1.Certificate) error + +func getCertificateForKey(ctx context.Context, key string, lister cmlisters.CertificateLister) (*v1alpha1.Certificate, error) { + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return nil, nil + } + + crt, err := lister.Certificates(namespace).Get(name) + if k8sErrors.IsNotFound(err) { + return nil, nil + } + if err != nil { + return nil, err + } + + return crt, nil +} + +func certificateGetter(lister cmlisters.CertificateLister) func(namespace, name string) (interface{}, error) { + return func(namespace, name string) (interface{}, error) { + return lister.Certificates(namespace).Get(name) + } } var keyFunc = controllerpkg.KeyFunc diff --git a/pkg/controller/certificates/sync.go b/pkg/controller/certificates/sync.go index 3f9063587..daef14f79 100644 --- a/pkg/controller/certificates/sync.go +++ b/pkg/controller/certificates/sync.go @@ -34,15 +34,17 @@ import ( "k8s.io/apimachinery/pkg/labels" utilerrors "k8s.io/apimachinery/pkg/util/errors" utilfeature "k8s.io/apiserver/pkg/util/feature" + corelisters "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" apiutil "github.com/jetstack/cert-manager/pkg/api/util" - "github.com/jetstack/cert-manager/pkg/apis/certmanager" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack/cert-manager/pkg/apis/certmanager/validation" + cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" "github.com/jetstack/cert-manager/pkg/feature" "github.com/jetstack/cert-manager/pkg/issuer" logf "github.com/jetstack/cert-manager/pkg/logs" + "github.com/jetstack/cert-manager/pkg/metrics" "github.com/jetstack/cert-manager/pkg/util" "github.com/jetstack/cert-manager/pkg/util/errors" "github.com/jetstack/cert-manager/pkg/util/kube" @@ -80,15 +82,15 @@ func (c *controller) Sync(ctx context.Context, crt *v1alpha1.Certificate) (err e log := logf.FromContext(ctx) dbg := log.V(logf.DebugLevel) - // TODO: if not 'certmanager.k8s.io, then use CertificateRequest stratagy if feature gate set - if !(crt.Spec.IssuerRef.Group == "" || crt.Spec.IssuerRef.Group == certmanager.GroupName) { - dbg.Info("certificate issuerRef group does not match certmanager group so skipping processing") + // if group name is set, use the new experimental controller implementation + if crt.Spec.IssuerRef.Group != "" { + log.Info("certificate issuerRef group is non-empty, skipping processing") return nil } crtCopy := crt.DeepCopy() defer func() { - if _, saveErr := c.updateCertificateStatus(ctx, crt, crtCopy); saveErr != nil { + if _, saveErr := updateCertificateStatus(ctx, c.metrics, c.cmClient, crt, crtCopy); saveErr != nil { err = utilerrors.NewAggregate([]error{saveErr, err}) } }() @@ -188,7 +190,7 @@ func (c *controller) Sync(ctx context.Context, crt *v1alpha1.Certificate) (err e } // begin checking if the TLS certificate is valid/needs a re-issue or renew - matches, matchErrs := c.certificateMatchesSpec(crtCopy, key, cert) + matches, matchErrs := certificateMatchesSpec(crtCopy, key, cert, c.secretLister) if !matches { dbg.Info("invoking issue function due to certificate not matching spec", "diff", strings.Join(matchErrs, ", ")) return c.issue(ctx, i, crtCopy) @@ -205,7 +207,7 @@ func (c *controller) Sync(ctx context.Context, crt *v1alpha1.Certificate) (err e dbg.Info("Certificate does not need updating. Scheduling renewal.") // If the Certificate is valid and up to date, we schedule a renewal in // the future. - c.scheduleRenewal(ctx, crt) + scheduleRenewal(ctx, c.secretLister, c.calculateDurationUntilRenew, c.scheduledWorkQueue.Add, crt) return nil } @@ -222,7 +224,7 @@ func (c *controller) setCertificateStatus(crt *v1alpha1.Certificate, key crypto. crt.Status.NotAfter = &metaNotAfter // Derive & set 'Ready' condition on Certificate resource - matches, matchErrs := c.certificateMatchesSpec(crt, key, cert) + matches, matchErrs := certificateMatchesSpec(crt, key, cert, c.secretLister) ready := v1alpha1.ConditionFalse reason := "" message := "" @@ -249,7 +251,7 @@ func (c *controller) setCertificateStatus(crt *v1alpha1.Certificate, key crypto. return } -func (c *controller) certificateMatchesSpec(crt *v1alpha1.Certificate, key crypto.Signer, cert *x509.Certificate) (bool, []string) { +func certificateMatchesSpec(crt *v1alpha1.Certificate, key crypto.Signer, cert *x509.Certificate, secretLister corelisters.SecretLister) (bool, []string) { var errs []string // TODO: add checks for KeySize, KeyAlgorithm fields @@ -284,7 +286,7 @@ func (c *controller) certificateMatchesSpec(crt *v1alpha1.Certificate, key crypt // get a copy of the current secret resource // Note that we already know that it exists, no need to check for errors // TODO: Refactor so that the secret is passed as argument? - secret, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + secret, err := secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) // validate that the issuer is correct if crt.Spec.IssuerRef.Name != secret.Annotations[v1alpha1.IssuerNameAnnotationKey] { @@ -292,14 +294,14 @@ func (c *controller) certificateMatchesSpec(crt *v1alpha1.Certificate, key crypt } // validate that the issuer kind is correct - if issuerKind(crt) != secret.Annotations[v1alpha1.IssuerKindAnnotationKey] { + if issuerKind(crt.Spec.IssuerRef) != secret.Annotations[v1alpha1.IssuerKindAnnotationKey] { errs = append(errs, fmt.Sprintf("Issuer kind of the certificate is not up to date: %q", secret.Annotations[v1alpha1.IssuerKindAnnotationKey])) } return len(errs) == 0, errs } -func (c *controller) scheduleRenewal(ctx context.Context, crt *v1alpha1.Certificate) { +func scheduleRenewal(ctx context.Context, lister corelisters.SecretLister, calc calculateDurationUntilRenewFn, queueFn func(interface{}, time.Duration), crt *v1alpha1.Certificate) { log := logf.FromContext(ctx) log = log.WithValues( logf.RelatedResourceNameKey, crt.Spec.SecretName, @@ -313,7 +315,7 @@ func (c *controller) scheduleRenewal(ctx context.Context, crt *v1alpha1.Certific return } - cert, err := kube.SecretTLSCert(ctx, c.secretLister, crt.Namespace, crt.Spec.SecretName) + cert, err := kube.SecretTLSCert(ctx, lister, crt.Namespace, crt.Spec.SecretName) if err != nil { if !errors.IsInvalidData(err) { log.Error(err, "error getting secret for certificate resource") @@ -321,18 +323,18 @@ func (c *controller) scheduleRenewal(ctx context.Context, crt *v1alpha1.Certific return } - renewIn := c.calculateDurationUntilRenew(ctx, cert, crt) - c.scheduledWorkQueue.Add(key, renewIn) + renewIn := calc(ctx, cert, crt) + queueFn(key, renewIn) log.WithValues("duration_until_renewal", renewIn.String()).Info("certificate scheduled for renewal") } // issuerKind returns the kind of issuer for a certificate -func issuerKind(crt *v1alpha1.Certificate) string { - if crt.Spec.IssuerRef.Kind == "" { +func issuerKind(ref v1alpha1.ObjectReference) string { + if ref.Kind == "" { return v1alpha1.IssuerKind } - return crt.Spec.IssuerRef.Kind + return ref.Kind } func ownerRef(crt *v1alpha1.Certificate) metav1.OwnerReference { @@ -448,7 +450,7 @@ func (c *controller) updateSecret(ctx context.Context, crt *v1alpha1.Certificate // not just when a new certificate is issued if x509Cert != nil { secret.Annotations[v1alpha1.IssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name - secret.Annotations[v1alpha1.IssuerKindAnnotationKey] = issuerKind(crt) + secret.Annotations[v1alpha1.IssuerKindAnnotationKey] = issuerKind(crt.Spec.IssuerRef) secret.Annotations[v1alpha1.CommonNameAnnotationKey] = x509Cert.Subject.CommonName secret.Annotations[v1alpha1.AltNamesAnnotationKey] = strings.Join(x509Cert.DNSNames, ",") secret.Annotations[v1alpha1.IPSANAnnotationKey] = strings.Join(pki.IPAddressesToString(x509Cert.IPAddresses), ",") @@ -502,7 +504,7 @@ func (c *controller) issue(ctx context.Context, issuer issuer.Interface, crt *v1 if len(resp.Certificate) > 0 { c.recorder.Event(crt, corev1.EventTypeNormal, successCertificateIssued, "Certificate issued successfully") // as we have just written a certificate, we should schedule it for renewal - c.scheduleRenewal(ctx, crt) + scheduleRenewal(ctx, c.secretLister, c.calculateDurationUntilRenew, c.scheduledWorkQueue.Add, crt) } return nil @@ -584,8 +586,8 @@ func generateLocallySignedTemporaryCertificate(crt *v1alpha1.Certificate, pk []b return b, nil } -func (c *controller) updateCertificateStatus(ctx context.Context, old, new *v1alpha1.Certificate) (*v1alpha1.Certificate, error) { - defer c.metrics.UpdateCertificateStatus(new) +func updateCertificateStatus(ctx context.Context, m *metrics.Metrics, cmClient cmclient.Interface, old, new *v1alpha1.Certificate) (*v1alpha1.Certificate, error) { + defer m.UpdateCertificateStatus(new) log := logf.FromContext(ctx, "updateStatus") oldBytes, _ := json.Marshal(old.Status) @@ -597,5 +599,5 @@ func (c *controller) updateCertificateStatus(ctx context.Context, old, new *v1al // TODO: replace Update call with UpdateStatus. This requires a custom API // server with the /status subresource enabled and/or subresource support // for CRDs (https://github.com/kubernetes/kubernetes/issues/38113) - return c.cmClient.CertmanagerV1alpha1().Certificates(new.Namespace).Update(new) + return cmClient.CertmanagerV1alpha1().Certificates(new.Namespace).Update(new) } diff --git a/pkg/issuer/acme/issue.go b/pkg/issuer/acme/issue.go index 5e0ed2e92..acca01f45 100644 --- a/pkg/issuer/acme/issue.go +++ b/pkg/issuer/acme/issue.go @@ -305,7 +305,7 @@ func (a *Acme) createNewOrder(ctx context.Context, crt *v1alpha1.Certificate, te log.V(4).Info("Creating new Order resource for Certificate") - csr, err := pki.GenerateCSR(a.issuer, crt) + csr, err := pki.GenerateCSR(crt) if err != nil { // TODO: what errors can be produced here? some error types might // be permanent, and we should handle that properly. diff --git a/pkg/issuer/vault/issue.go b/pkg/issuer/vault/issue.go index efb8f5e3e..a5b03da60 100644 --- a/pkg/issuer/vault/issue.go +++ b/pkg/issuer/vault/issue.go @@ -72,7 +72,7 @@ func (v *Vault) Issue(ctx context.Context, crt *v1alpha1.Certificate) (*issuer.I /// BEGIN building CSR // TODO: we should probably surface some of these errors to users - template, err := pki.GenerateCSR(v.issuer, crt) + template, err := pki.GenerateCSR(crt) if err != nil { return nil, err } diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index c4f4e87a2..792310c12 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -113,7 +113,7 @@ var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) // by issuers that utilise CSRs to obtain Certificates. // The CSR will not be signed, and should be passed to either EncodeCSR or // to the x509.CreateCertificateRequest function. -func GenerateCSR(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) (*x509.CertificateRequest, error) { +func GenerateCSR(crt *v1alpha1.Certificate) (*x509.CertificateRequest, error) { commonName := CommonNameForCertificate(crt) dnsNames := DNSNamesForCertificate(crt) iPAddresses := IPAddressesForCertificate(crt) diff --git a/test/unit/gen/certificate.go b/test/unit/gen/certificate.go index 89c484f37..1fe4911ef 100644 --- a/test/unit/gen/certificate.go +++ b/test/unit/gen/certificate.go @@ -17,6 +17,8 @@ limitations under the License. package gen import ( + "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" @@ -91,6 +93,18 @@ func SetCertificateSecretName(secretName string) CertificateModifier { } } +func SetCertificateDuration(duration time.Duration) CertificateModifier { + return func(crt *v1alpha1.Certificate) { + crt.Spec.Duration = &metav1.Duration{Duration: duration} + } +} + +func SetCertificateRenewBefore(renewBefore time.Duration) CertificateModifier { + return func(crt *v1alpha1.Certificate) { + crt.Spec.RenewBefore = &metav1.Duration{Duration: renewBefore} + } +} + func SetCertificateStatusCondition(c v1alpha1.CertificateCondition) CertificateModifier { return func(crt *v1alpha1.Certificate) { if len(crt.Status.Conditions) == 0 { diff --git a/test/unit/gen/certificaterequest.go b/test/unit/gen/certificaterequest.go index 872dd75bc..ba7d887df 100644 --- a/test/unit/gen/certificaterequest.go +++ b/test/unit/gen/certificaterequest.go @@ -100,3 +100,9 @@ func SetCertificateRequestNamespace(namespace string) CertificateRequestModifier cr.ObjectMeta.Namespace = namespace } } + +func SetCertificateRequestName(name string) CertificateRequestModifier { + return func(cr *v1alpha1.CertificateRequest) { + cr.ObjectMeta.Name = name + } +}