diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index 3c551f3..1780d5b 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -5,6 +5,7 @@ "strings" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/issuing" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/util/sets" @@ -35,11 +36,13 @@ // certificate controllers issuing.ControllerName, issuers.ControllerName, + trigger.ControllerName, } defaultEnabledControllers = []string{ issuing.ControllerName, issuers.ControllerName, + trigger.ControllerName, } ) diff --git a/cmd/controller/app/start.go b/cmd/controller/app/start.go index 3a3d0d3..b2cf3f0 100644 --- a/cmd/controller/app/start.go +++ b/cmd/controller/app/start.go @@ -4,6 +4,7 @@ "fmt" options "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/cmd/controller/app/options" + _ "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/trigger" _ "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/issuers" _ "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/issuer/selfsigned" logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" diff --git a/config/crd/bases/anthos-cert-manager.io_certificaterequests.yaml b/config/crd/bases/anthos-cert-manager.io_certificaterequests.yaml index c4f98ff..522ea4e 100644 --- a/config/crd/bases/anthos-cert-manager.io_certificaterequests.yaml +++ b/config/crd/bases/anthos-cert-manager.io_certificaterequests.yaml @@ -216,3 +216,5 @@ type: object served: true storage: true + subresources: + status: {} diff --git a/config/crd/bases/anthos-cert-manager.io_certificates.yaml b/config/crd/bases/anthos-cert-manager.io_certificates.yaml index b5e3df3..8cc62de 100644 --- a/config/crd/bases/anthos-cert-manager.io_certificates.yaml +++ b/config/crd/bases/anthos-cert-manager.io_certificates.yaml @@ -385,3 +385,5 @@ type: object served: true storage: true + subresources: + status: {} diff --git a/pkg/api/util/conditions.go b/pkg/api/util/conditions.go index 9ec59cc..829ca62 100644 --- a/pkg/api/util/conditions.go +++ b/pkg/api/util/conditions.go @@ -111,3 +111,48 @@ i.GetStatus().Conditions = append(i.GetStatus().Conditions, newCondition) logf.V(logf.InfoLevel).Infof("Setting lastTransitionTime for Issuer %q condition %q to %v", i.GetObjectMeta().Name, conditionType, nowTime.Time) } + +// SetCertificateCondition will set a 'condition' on the given Certificate. +func SetCertificateCondition(crt *acmapi.Certificate, observedGeneration int64, conditionType acmapi.CertificateConditionType, status acmmeta.ConditionStatus, reason, message string) { + newCondition := acmapi.CertificateCondition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + } + + nowtime := metav1.NewTime(Clock.Now()) + newCondition.LastTransitionTime = &nowtime + newCondition.ObservedGeneration = observedGeneration + + for idx, cond := range crt.Status.Conditions { + if cond.Type != conditionType { + continue + } + + // If the status is not changed, keep the original time. + if cond.Status == status { + newCondition.LastTransitionTime = cond.LastTransitionTime + } else { + logf.V(logf.InfoLevel).Infof("Found status change for Certificate %q condition %q: %q -> %q; setting lastTransitionTime to %v", crt.Name, conditionType, cond.Status, status, nowtime.Time) + } + + //override the existing condition + crt.Status.Conditions[idx] = newCondition + return + } + + // If we could not find the condition, we just insert it to the slice. + crt.Status.Conditions = append(crt.Status.Conditions, newCondition) + logf.V(logf.InfoLevel).Infof("Setting lastTransitionTime for Certificate %q condition %q to %v", crt.Name, conditionType, nowtime.Time) +} + +func GetCertificateCondition(crt *acmapi.Certificate, conditionType acmapi.CertificateConditionType) *acmapi.CertificateCondition { + + for _, cond := range crt.Status.Conditions { + if cond.Type == conditionType { + return &cond + } + } + return nil +} diff --git a/pkg/apis/anthoscertmanager/v1/certificate_types.go b/pkg/apis/anthoscertmanager/v1/certificate_types.go index 739b2f3..80b0123 100644 --- a/pkg/apis/anthoscertmanager/v1/certificate_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificate_types.go @@ -361,6 +361,7 @@ // +k8s:openapi-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:storageversion +// +kubebuilder:subresource:status // Certificate is the Schema for the certificates API type Certificate struct { diff --git a/pkg/apis/anthoscertmanager/v1/certificaterequest_types.go b/pkg/apis/anthoscertmanager/v1/certificaterequest_types.go index 7175fa0..0ab5cd1 100644 --- a/pkg/apis/anthoscertmanager/v1/certificaterequest_types.go +++ b/pkg/apis/anthoscertmanager/v1/certificaterequest_types.go @@ -26,6 +26,8 @@ // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +k8s:openapi-gen=true // A CertificateRequest is used to request a signed certificate from one of the // configured issuers. @@ -36,7 +38,6 @@ // // A CertificateRequest is a one-shot resource, meaning it represents a single // point in time request for a certificate and cannot be re-used. -// +k8s:openapi-gen=true type CertificateRequest struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/apis/anthoscertmanager/v1/issuer_types.go b/pkg/apis/anthoscertmanager/v1/issuer_types.go index 7b46656..d47772f 100644 --- a/pkg/apis/anthoscertmanager/v1/issuer_types.go +++ b/pkg/apis/anthoscertmanager/v1/issuer_types.go @@ -68,11 +68,11 @@ metav1.ObjectMeta `json:"metadata,omitempty"` // Desired state of the Issuer resource. - Spec IssuerSpec `json:"spec,omitempty"` + Spec IssuerSpec `json:"spec"` // Status of the Issuer. This is set and managed automatically. // +optional - Status IssuerStatus `json:"status,omitempty"` + Status IssuerStatus `json:"status"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/controller/certificates/issuing/issuing_controller.go b/pkg/controller/certificates/issuing/issuing_controller.go index 72bda06..1c89bc7 100644 --- a/pkg/controller/certificates/issuing/issuing_controller.go +++ b/pkg/controller/certificates/issuing/issuing_controller.go @@ -8,8 +8,8 @@ apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" - acmClient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" - acmInformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" @@ -46,7 +46,7 @@ secretLister corelisters.SecretLister recorder record.EventRecorder clock clock.Clock - client acmClient.Interface + client acmclient.Interface // postIssuancePolicyChain is the policies chain to ensure that all Secret // metadata and output formats are kept are present and correct. @@ -61,9 +61,9 @@ func NewController( log logr.Logger, kubeClient kubernetes.Interface, - client acmClient.Interface, + client acmclient.Interface, factory informers.SharedInformerFactory, - acmFactory acmInformers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, recorder record.EventRecorder, clock clock.Clock, certificateControllerOptions controllerpkg.CertificateOptions, diff --git a/pkg/controller/certificates/policies/checks.go b/pkg/controller/certificates/policies/checks.go index 8adce6b..924d5ed 100644 --- a/pkg/controller/certificates/policies/checks.go +++ b/pkg/controller/certificates/policies/checks.go @@ -1,5 +1,18 @@ package policies +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/pki" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/utils/clock" +) + // SecretOwnerReferenceManagedFieldMismatch validates that the Secret has an // owner reference to the Certificate if enabled. Returns true (violation) if: // * the Secret doesn't have an owner reference and is expecting one @@ -12,3 +25,67 @@ } } + +func SecretDoesNotExist(input Input) (string, string, bool) { + if input.Secret == nil { + return DoesNotExist, "Issuing certificate as Secret does not exist", true + } + return "", "", false +} + +func SecretIssuerAnnotationsNotUpToDate(input Input) (string, string, bool) { + name := input.Secret.Annotations[acmapi.IssuerNameAnnotationKey] + kind := input.Secret.Annotations[acmapi.IssuerKindAnnotationKey] + group := input.Secret.Annotations[acmapi.IssuerGroupAnnotationKey] + + if name != input.Certificate.Spec.IssuerRef.Name || + kind != input.Certificate.Spec.IssuerRef.Kind || + group != input.Certificate.Spec.IssuerRef.Group { + return IncorrectIssuer, fmt.Sprintf("Issuing certificate as Secret was previously issued by %s", formatIssuerRef(name, kind, group)), true + } + return "", "", false + +} + +// CurrentCertificateNearingExpiry returns a policy function that can be used to +// check whether an X.509 cert currently issued for a Certificate should be +// renewed. +func CurrentCertificateNearingExpiry(c clock.Clock) Func { + + return func(input Input) (string, string, bool) { + + // Determine if the certificate is nearing expiry solely by looking at + // the actual cert, if it exists. We assume that at this point we have + // called policy functions that check that input.Secret and + // input.Secret.Data exists (SecretDoesNotExist and SecretIsMissingData). + x509cert, err := pki.DecodeX509CertificateBytes(input.Secret.Data[corev1.TLSCertKey]) + if err != nil { + // This case should never happen as it should always be caught by the + // secretPublicKeysMatch function beforehand, but handle it just in case. + return InvalidCertificate, fmt.Sprintf("Failed to decode stored certificate: %v", err), true + } + + notBefore := metav1.NewTime(x509cert.NotBefore) + notAfter := metav1.NewTime(x509cert.NotAfter) + crt := input.Certificate + renewalTime := certificates.RenewalTime(notBefore.Time, notAfter.Time, crt.Spec.RenewBefore) + + renewIn := renewalTime.Time.Sub(c.Now()) + if renewIn > 0 { + //renewal time is in future, no need to renew + return "", "", false + } + + return Renewing, fmt.Sprintf("Renewing certificate as renewal was scheduled at %s", input.Certificate.Status.RenewalTime), true + } +} + +func formatIssuerRef(name, kind, group string) string { + if group == "" { + group = "anthos-cert-manager.io" + } + if kind == "" { + kind = "Issuer" + } + return fmt.Sprintf("%s.%s/%s", kind, group, name) +} diff --git a/pkg/controller/certificates/policies/constants.go b/pkg/controller/certificates/policies/constants.go new file mode 100644 index 0000000..f63f251 --- /dev/null +++ b/pkg/controller/certificates/policies/constants.go @@ -0,0 +1,46 @@ +package policies + +const ( + // DoesNotExist is a policy violation reason for a scenario where + // Certificate's spec.secretName secret does not exist. + DoesNotExist string = "DoesNotExist" + // MissingData is a policy violation reason for a scenario where + // Certificate's spec.secretName secret has missing data. + MissingData string = "MissingData" + // InvalidKeyPair is a policy violation reason for a scenario where public + // key of certificate does not match private key. + InvalidKeyPair string = "InvalidKeyPair" + // InvalidCertificate is a policy violation whereby the signed certificate in + // the Input Secret could not be parsed or decoded. + InvalidCertificate string = "InvalidCertificate" + // SecretMismatch is a policy violation reason for a scenario where Secret's + // private key does not match spec. + SecretMismatch string = "SecretMismatch" + // IncorrectIssuer is a policy violation reason for a scenario where + // Certificate has been issued by incorrect Issuer. + IncorrectIssuer string = "IncorrectIssuer" + // RequestChanged is a policy violation reason for a scenario where + // CertificateRequest not valid for Certificate's spec. + RequestChanged string = "RequestChanged" + // Renewing is a policy violation reason for a scenario where + // Certificate's renewal time is now or in past. + Renewing string = "Renewing" + // Expired is a policy violation reason for a scenario where Certificate has + // expired. + Expired string = "Expired" + // SecretTemplateMisMatch is a policy violation whereby the Certificate's + // SecretTemplate is not reflected on the target Secret, either by having + // extra, missing, or wrong Annotations or Labels. + SecretTemplateMismatch string = "SecretTemplateMismatch" + // AdditionalOutputFormatsMismatch is a policy violation whereby the + // Certificate's AdditionalOutputFormats is not reflected on the target + // Secret, either by having extra, missing, or wrong values. + AdditionalOutputFormatsMismatch string = "AdditionalOutputFormatsMismatch" + // ManagedFieldsParseError is a policy violation whereby cert-manager was + // unable to decode the managed fields on a resource. + ManagedFieldsParseError string = "ManagedFieldsParseError" + // SecretOwnerRefMismatch is a policy violation whereby the Secret either has + // a missing owner reference to the Certificate, or has an owner reference it + // shouldn't have. + SecretOwnerRefMismatch string = "SecretOwnerRefMismatch" +) diff --git a/pkg/controller/certificates/policies/gather.go b/pkg/controller/certificates/policies/gather.go new file mode 100644 index 0000000..1fab96f --- /dev/null +++ b/pkg/controller/certificates/policies/gather.go @@ -0,0 +1,62 @@ +package policies + +import ( + "context" + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + corelisters "k8s.io/client-go/listers/core/v1" +) + +// Gatherer is used to gather data about a Certificate in order to evaluate +// its current readiness/state by applying policy functions to it. +type Gatherer struct { + CertificateRequestLister acmlisters.CertificateRequestLister + SecretLister corelisters.SecretLister +} + +// DataForCertificate returns the secret as well as the +// certificate request associated with the given certificate. +func (g *Gatherer) DataForCertificate(ctx context.Context, crt *acmapi.Certificate) (Input, error) { + log := logf.FromContext(ctx) + + // attempts to fetch the secret being managed. + secret, err := g.SecretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName) + if err != nil && apierrors.IsNotFound(err) { + return Input{}, err + } + + // For the first time creation, the Status.Revision is nil, so we can skip the check. + var curCr *acmapi.CertificateRequest + if crt.Status.Revision != nil { + reqs, err := certificates.ListCertificateRequestsMatchingPredicates(g.CertificateRequestLister.CertificateRequests(crt.Namespace), + labels.Everything(), + predicate.ResourceOwnedBy(crt), + predicate.CertificateRequestRevision(*crt.Status.Revision)) + + if err != nil { + return Input{}, err + } + + switch { + case len(reqs) > 1: + return Input{}, fmt.Errorf("multiple CertificateRequests were found for the 'current' revision %v, issuance is skipped until there are no more duplicates", *crt.Status.Revision) + case len(reqs) == 1: + curCr = reqs[0] + case len(reqs) == 0: + log.V(logf.DebugLevel).Info("Found no CertificateRequest resources owned by this Certificate for the current revision", "revision", *crt.Status.Revision) + } + } + + return Input{ + Certificate: crt, + Secret: secret, + CurrentRevisionRequest: curCr, + }, nil +} diff --git a/pkg/controller/certificates/policies/policies.go b/pkg/controller/certificates/policies/policies.go index 7fc5eca..59111bc 100644 --- a/pkg/controller/certificates/policies/policies.go +++ b/pkg/controller/certificates/policies/policies.go @@ -2,6 +2,7 @@ import ( corev1 "k8s.io/api/core/v1" + "k8s.io/utils/clock" acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" ) @@ -10,13 +11,13 @@ Certificate *acmapi.Certificate Secret *corev1.Secret - // // The "current" certificate request designates the certificate request that - // // led to the current revision of the certificate. The "current" certificate - // // request is by definition in a ready state, and can be seen as the source - // // of information of the current certificate. Take a look at the gatherer - // // package's documentation to see more about why we care about the "current" - // // certificate request. - // CurrentRevisionRequest *cmapi.CertificateRequest + // The "current" certificate request designates the certificate request that + // led to the current revision of the certificate. The "current" certificate + // request is by definition in a ready state, and can be seen as the source + // of information of the current certificate. Take a look at the gatherer + // package's documentation to see more about why we care about the "current" + // certificate request. + CurrentRevisionRequest *acmapi.CertificateRequest // // The "next" certificate request is the one that is currently being issued. // // Take a look at the gatherer package's documentation to see more about why @@ -51,3 +52,17 @@ // TODO: Check the owner referience value mismatch return Chain{SecretOwnerReferenceManagedFieldMismatch(ownerRefEnabled, fieldManager)} } + +// NewTriggerPolicyChain includes trigger policy checks, which if return true, +// should cause a Certificate to be marked for issuance. +func NewTriggerPolicyChain(c clock.Clock) Chain { + return Chain{ + SecretDoesNotExist, + // SecretIsMissingData, + // SecretPublicKeysDiffer, + // SecretPrivateKeyMatchesSpec, + SecretIssuerAnnotationsNotUpToDate, + // CurrentCertificateRequestNotValidForSpec, + // CurrentCertificateNearingExpiry(c), + } +} diff --git a/pkg/controller/certificates/trigger/trigger_controller.go b/pkg/controller/certificates/trigger/trigger_controller.go new file mode 100644 index 0000000..148e48f --- /dev/null +++ b/pkg/controller/certificates/trigger/trigger_controller.go @@ -0,0 +1,202 @@ +package trigger + +import ( + "context" + "time" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + acmmeta "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/meta/v1" + acmclient "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/clientset/versioned" + acmlisters "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/listers/anthoscertmanager/v1" + controllerpkg "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates" + "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/util/predicate" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + apiutil "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/api/util" + acminformers "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/client/informers/externalversions" + policies "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/controller/certificates/policies" + logf "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/logs" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + 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" +) + +const ( + ControllerName = "certificates-trigger" +) + +type controller struct { + certificateLister acmlisters.CertificateLister + certificateRequestLister acmlisters.CertificateRequestLister + secretLister corelisters.SecretLister + client acmclient.Interface + recorder record.EventRecorder + // scheduledWorkQueue scheduler.ScheduledWorkQueue + + // fieldManager is the string which will be used as the Field Manager on + // fields created or edited by the cert-manager Kubernetes client during + // Apply API calls. + fieldManager string + + // The following are used for testing purposes. + clock clock.Clock + shouldReissue policies.Func + dataForCertificate func(context.Context, *acmapi.Certificate) (policies.Input, error) +} + +func NewController(log logr.Logger, + client acmclient.Interface, + factory informers.SharedInformerFactory, + acmFactory acminformers.SharedInformerFactory, + recorder record.EventRecorder, + clock clock.Clock, + shouldReissue policies.Func, + fieldManager string, +) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) { + + // create a queue used to queue up items to be processed. + queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName) + + // obtain references to all the informers used by this controller + certificateInformer := acmFactory.AnthosCertmanager().V1().Certificates() + certificateRequestInformer := acmFactory.AnthosCertmanager().V1().CertificateRequests() + secretsInformer := factory.Core().V1().Secrets() + + // Certificate events will be handled asynchonizelly + certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + // When a CertificateRequest resource changes, enqueue the Certificate resource that owns it. + certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), predicate.ResourceOwnerOf), + }) + + // When a secret resource changes, enqueue any certificate resource that name it as spec.SecretName + secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), predicate.ResourceOwnerOf), + }) + + // 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, + } + + return &controller{ + certificateLister: certificateInformer.Lister(), + certificateRequestLister: certificateRequestInformer.Lister(), + secretLister: secretsInformer.Lister(), + client: client, + recorder: recorder, + //scheduledWorkQueue: scheduler.NewScheduledWorkQueue(clock, queue.Add), + fieldManager: fieldManager, + + // The following are used for testing purposes. + clock: clock, + shouldReissue: shouldReissue, + dataForCertificate: (&policies.Gatherer{ + CertificateRequestLister: certificateRequestInformer.Lister(), + SecretLister: secretsInformer.Lister(), + }).DataForCertificate, + }, queue, mustSync +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx).WithValues("key", key) + ctx = logf.NewContext(ctx, log) + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key passed to ProcessItem") + return nil + } + + crt, err := c.certificateLister.Certificates(namespace).Get(name) + if apierrors.IsNotFound(err) { + log.V(logf.DebugLevel).Info("certificate not found for key", "error", err.Error()) + return nil + } + if err != nil { + return err + } + + if apiutil.CertificateHasCondition(crt, acmapi.CertificateCondition{ + Type: acmapi.CertificateConditionIssuing, + Status: acmmeta.ConditionTrue, + }) { + // Do nothing if an issuance is already in progress + return nil + } + + input, err := c.dataForCertificate(ctx, crt) + if err != nil { + return err + } + + if crt.Status.RenewalTime != nil { + //TODO Implement a scheduler to recheck if the certificate is near the expire + } + + reason, message, reissue := c.shouldReissue(input) + + if !reissue { + // no re-issuance required + return nil + } + + log.V(logf.InfoLevel).Info("Certificate must be re-issued", "reason", reason, "message", message) + crt = crt.DeepCopy() + apiutil.SetCertificateCondition(crt, crt.Generation, acmapi.CertificateConditionIssuing, acmmeta.ConditionTrue, reason, message) + if err := c.updateOrApplyStatus(ctx, crt); err != nil { + return err + } + c.recorder.Event(crt, corev1.EventTypeNormal, "Issuing", message) + + return nil +} + +func (c *controller) updateOrApplyStatus(ctx context.Context, crt *acmapi.Certificate) error { + _, err := c.client.AnthosCertmanagerV1().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{}) + return err +} + +// controllerWrapper wraps the `controller` structure to make it implement +// the controllerpkg.queueingController interface +type controllerWrapper struct { + *controller +} + +func (c *controllerWrapper) 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, ControllerName) + + ctrl, queue, mustSync := NewController(log, + ctx.ACMClient, + ctx.KubeSharedInformerFactory, + ctx.SharedInformerFactory, + ctx.Recorder, + ctx.Clock, + policies.NewTriggerPolicyChain(ctx.Clock).Evaluate, + ctx.FieldManager, + ) + c.controller = ctrl + + return queue, mustSync, nil +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.ContextFactory) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controllerWrapper{}). + Complete() + }) +} diff --git a/pkg/controller/certificates/utils.go b/pkg/controller/certificates/utils.go new file mode 100644 index 0000000..16b1b26 --- /dev/null +++ b/pkg/controller/certificates/utils.go @@ -0,0 +1,45 @@ +package certificates + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RenewalTimeFunc is a custom function type for calculating renewal time of a certificate. +type RenewalTimeFunc func(time.Time, time.Time, *metav1.Duration) *metav1.Time + +// RenewalTime calculates renewal time for a certificate. Default renewal time +// is 2/3 through certificate's lifetime. If user has configured +// spec.renewBefore, renewal time will be renewBefore period before expiry +// (unless that is after the expiry). +func RenewalTime(notBefore, notAfter time.Time, renewBeforeOverride *metav1.Duration) *metav1.Time { + + // 1. Calculate how long before expiry a cert should be renewed + + actualDuration := notAfter.Sub(notBefore) + + renewBefore := actualDuration / 3 + + // If spec.renewBefore was set (and is less than duration) + // respect that. We don't want to prevent users from renewing + // longer lived certs more frequently. + if renewBeforeOverride != nil && renewBeforeOverride.Duration < actualDuration { + renewBefore = renewBeforeOverride.Duration + } + + // 2. Calculate when a cert should be renewed + + // Truncate the renewal time to nearest second. This is important + // because the renewal time also gets stored on Certificate's status + // where it is truncated to the nearest second. We use the renewal time + // from Certificate's status to determine when the Certificate will be + // added to the queue to be renewed, but then re-calculate whether it + // needs to be renewed _now_ using this function- so returning a + // non-truncated value here would potentially cause Certificates to be + // re-queued for renewal earlier than the calculated renewal time thus + // causing Certificates to not be automatically renewed. See + // https://github.com/cert-manager/cert-manager/pull/4399. + rt := metav1.NewTime(notAfter.Add(-1 * renewBefore).Truncate(time.Second)) + return &rt +} diff --git a/pkg/util/predicate/certificaterequest.go b/pkg/util/predicate/certificaterequest.go new file mode 100644 index 0000000..0ec3f68 --- /dev/null +++ b/pkg/util/predicate/certificaterequest.go @@ -0,0 +1,21 @@ +package predicate + +import ( + "fmt" + + acmapi "gitbucket.jerxie.com/yangyangxie/AnthosCertManager/pkg/apis/anthoscertmanager/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// CertificateRequestRevision returns a predicate that used to filter +// CertificateRequest to only those with a given 'revision' number. +func CertificateRequestRevision(revision int) Func { + return func(obj runtime.Object) bool { + req := obj.(*acmapi.CertificateRequest) + if req.Annotations == nil { + return false + } + + return req.Annotations[acmapi.CertificateRequestRevisionAnnotationKey] == fmt.Sprintf("%d", revision) + } +}