diff --git a/docs/generated/reference/output/reference/api-docs/index.html b/docs/generated/reference/output/reference/api-docs/index.html index 90ac3cd49..08ac8dd85 100755 --- a/docs/generated/reference/output/reference/api-docs/index.html +++ b/docs/generated/reference/output/reference/api-docs/index.html @@ -11,7 +11,7 @@ - +

cert-manager

@@ -97,6 +97,10 @@ Appears In: DNSNames is a list of subject alt names to be used on the Certificate +duration
Duration +Certificate default Duration + + isCA
boolean IsCA will mark this Certificate as valid for signing. This implies that the 'signing' usage is set @@ -117,6 +121,10 @@ Appears In: Organization is the organization to be used on the Certificate +renewBefore
Duration +Certificate renew before expiration duration + + secretName
string SecretName is the name of the secret resource to store this secret in @@ -1386,6 +1394,45 @@ Appears In: +

Duration v1

+ + + + + + + + + + + + + + + +
GroupVersionKind
metav1Duration
+

Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json.

+ + + + + + + + + + + + + + + +
FieldDescription
Duration
integer

HTTP01SolverConfig v1alpha1

diff --git a/docs/generated/reference/output/reference/api-docs/navData.js b/docs/generated/reference/output/reference/api-docs/navData.js index fa8246fcb..99e94cda7 100755 --- a/docs/generated/reference/output/reference/api-docs/navData.js +++ b/docs/generated/reference/output/reference/api-docs/navData.js @@ -1 +1 @@ -(function(){navData = {"toc":[{"section":"-strong-field-definitions-strong-","subsections":[{"section":"vaultissuer-v1alpha1"},{"section":"vaultauth-v1alpha1"},{"section":"vaultapprole-v1alpha1"},{"section":"time-v1"},{"section":"statusdetails-v1"},{"section":"statuscause-v1"},{"section":"status-v1"},{"section":"solverconfig-v1alpha1"},{"section":"selfsignedissuer-v1alpha1"},{"section":"secretkeyselector-v1alpha1"},{"section":"ownerreference-v1"},{"section":"objectreference-v1alpha1"},{"section":"objectmeta-v1"},{"section":"listmeta-v1"},{"section":"issuercondition-v1alpha1"},{"section":"initializers-v1"},{"section":"initializer-v1"},{"section":"http01solverconfig-v1alpha1"},{"section":"domainsolverconfig-v1alpha1"},{"section":"dns01solverconfig-v1alpha1"},{"section":"certificatecondition-v1alpha1"},{"section":"caissuer-v1alpha1"},{"section":"acmeissuerhttp01config-v1alpha1"},{"section":"acmeissuerdns01providerroute53-v1alpha1"},{"section":"acmeissuerdns01providerrfc2136-v1alpha1"},{"section":"acmeissuerdns01providerdigitalocean-v1alpha1"},{"section":"acmeissuerdns01providercloudflare-v1alpha1"},{"section":"acmeissuerdns01providerclouddns-v1alpha1"},{"section":"acmeissuerdns01providerazuredns-v1alpha1"},{"section":"acmeissuerdns01providerakamai-v1alpha1"},{"section":"acmeissuerdns01provideracmedns-v1alpha1"},{"section":"acmeissuerdns01provider-v1alpha1"},{"section":"acmeissuerdns01config-v1alpha1"},{"section":"acmeissuer-v1alpha1"},{"section":"acmecertificateconfig-v1alpha1"}]},{"section":"-strong-old-api-versions-strong-","subsections":[]},{"section":"challenge-v1alpha1","subsections":[]},{"section":"order-v1alpha1","subsections":[]},{"section":"issuer-v1alpha1","subsections":[]},{"section":"clusterissuer-v1alpha1","subsections":[]},{"section":"certificate-v1alpha1","subsections":[]},{"section":"-strong-cert-manager-strong-","subsections":[]}],"flatToc":["vaultissuer-v1alpha1","vaultauth-v1alpha1","vaultapprole-v1alpha1","time-v1","statusdetails-v1","statuscause-v1","status-v1","solverconfig-v1alpha1","selfsignedissuer-v1alpha1","secretkeyselector-v1alpha1","ownerreference-v1","objectreference-v1alpha1","objectmeta-v1","listmeta-v1","issuercondition-v1alpha1","initializers-v1","initializer-v1","http01solverconfig-v1alpha1","domainsolverconfig-v1alpha1","dns01solverconfig-v1alpha1","certificatecondition-v1alpha1","caissuer-v1alpha1","acmeissuerhttp01config-v1alpha1","acmeissuerdns01providerroute53-v1alpha1","acmeissuerdns01providerrfc2136-v1alpha1","acmeissuerdns01providerdigitalocean-v1alpha1","acmeissuerdns01providercloudflare-v1alpha1","acmeissuerdns01providerclouddns-v1alpha1","acmeissuerdns01providerazuredns-v1alpha1","acmeissuerdns01providerakamai-v1alpha1","acmeissuerdns01provideracmedns-v1alpha1","acmeissuerdns01provider-v1alpha1","acmeissuerdns01config-v1alpha1","acmeissuer-v1alpha1","acmecertificateconfig-v1alpha1","-strong-field-definitions-strong-","-strong-old-api-versions-strong-","challenge-v1alpha1","order-v1alpha1","issuer-v1alpha1","clusterissuer-v1alpha1","certificate-v1alpha1","-strong-cert-manager-strong-"]};})(); \ No newline at end of file +(function(){navData = {"toc":[{"section":"-strong-field-definitions-strong-","subsections":[{"section":"vaultissuer-v1alpha1"},{"section":"vaultauth-v1alpha1"},{"section":"vaultapprole-v1alpha1"},{"section":"time-v1"},{"section":"statusdetails-v1"},{"section":"statuscause-v1"},{"section":"status-v1"},{"section":"solverconfig-v1alpha1"},{"section":"selfsignedissuer-v1alpha1"},{"section":"secretkeyselector-v1alpha1"},{"section":"ownerreference-v1"},{"section":"objectreference-v1alpha1"},{"section":"objectmeta-v1"},{"section":"listmeta-v1"},{"section":"issuercondition-v1alpha1"},{"section":"initializers-v1"},{"section":"initializer-v1"},{"section":"http01solverconfig-v1alpha1"},{"section":"duration-v1"},{"section":"domainsolverconfig-v1alpha1"},{"section":"dns01solverconfig-v1alpha1"},{"section":"certificatecondition-v1alpha1"},{"section":"caissuer-v1alpha1"},{"section":"acmeissuerhttp01config-v1alpha1"},{"section":"acmeissuerdns01providerroute53-v1alpha1"},{"section":"acmeissuerdns01providerrfc2136-v1alpha1"},{"section":"acmeissuerdns01providerdigitalocean-v1alpha1"},{"section":"acmeissuerdns01providercloudflare-v1alpha1"},{"section":"acmeissuerdns01providerclouddns-v1alpha1"},{"section":"acmeissuerdns01providerazuredns-v1alpha1"},{"section":"acmeissuerdns01providerakamai-v1alpha1"},{"section":"acmeissuerdns01provideracmedns-v1alpha1"},{"section":"acmeissuerdns01provider-v1alpha1"},{"section":"acmeissuerdns01config-v1alpha1"},{"section":"acmeissuer-v1alpha1"},{"section":"acmecertificateconfig-v1alpha1"}]},{"section":"-strong-old-api-versions-strong-","subsections":[]},{"section":"challenge-v1alpha1","subsections":[]},{"section":"order-v1alpha1","subsections":[]},{"section":"issuer-v1alpha1","subsections":[]},{"section":"clusterissuer-v1alpha1","subsections":[]},{"section":"certificate-v1alpha1","subsections":[]},{"section":"-strong-cert-manager-strong-","subsections":[]}],"flatToc":["vaultissuer-v1alpha1","vaultauth-v1alpha1","vaultapprole-v1alpha1","time-v1","statusdetails-v1","statuscause-v1","status-v1","solverconfig-v1alpha1","selfsignedissuer-v1alpha1","secretkeyselector-v1alpha1","ownerreference-v1","objectreference-v1alpha1","objectmeta-v1","listmeta-v1","issuercondition-v1alpha1","initializers-v1","initializer-v1","http01solverconfig-v1alpha1","duration-v1","domainsolverconfig-v1alpha1","dns01solverconfig-v1alpha1","certificatecondition-v1alpha1","caissuer-v1alpha1","acmeissuerhttp01config-v1alpha1","acmeissuerdns01providerroute53-v1alpha1","acmeissuerdns01providerrfc2136-v1alpha1","acmeissuerdns01providerdigitalocean-v1alpha1","acmeissuerdns01providercloudflare-v1alpha1","acmeissuerdns01providerclouddns-v1alpha1","acmeissuerdns01providerazuredns-v1alpha1","acmeissuerdns01providerakamai-v1alpha1","acmeissuerdns01provideracmedns-v1alpha1","acmeissuerdns01provider-v1alpha1","acmeissuerdns01config-v1alpha1","acmeissuer-v1alpha1","acmecertificateconfig-v1alpha1","-strong-field-definitions-strong-","-strong-old-api-versions-strong-","challenge-v1alpha1","order-v1alpha1","issuer-v1alpha1","clusterissuer-v1alpha1","certificate-v1alpha1","-strong-cert-manager-strong-"]};})(); \ No newline at end of file diff --git a/docs/reference/certificates.rst b/docs/reference/certificates.rst index 777df94fc..0e5f4974f 100644 --- a/docs/reference/certificates.rst +++ b/docs/reference/certificates.rst @@ -35,17 +35,18 @@ A simple Certificate could be defined as: This Certificate will tell cert-manager to attempt to use the Issuer named ``letsencrypt-prod`` to obtain a certificate key pair for the -``foo.example.com`` and ``bar.example.com`` domains. If successful, the resulting -key and certificate will be stored in a secret named ``acme-crt-secret`` with -keys of ``tls.key`` and ``tls.crt`` respectively. This secret will live in the -same namespace as the ``Certificate`` resource. +``foo.example.com`` and ``bar.example.com`` domains. If successful, the +resulting key and certificate will be stored in a secret named +``acme-crt-secret`` with keys of ``tls.key`` and ``tls.crt`` respectively. +This secret will live in the same namespace as the ``Certificate`` resource. The ``dnsNames`` field specifies a list of `Subject Alternative Names`_ to be associated with the certificate. If the ``commonName`` field is omitted, the first element in the list will be the common name. -The referenced Issuer must exist in the same namespace as the Certificate. A -Certificate can alternatively reference a ClusterIssuer which is non-namespaced. +The referenced Issuer must exist in the same namespace as the Certificate. +A Certificate can alternatively reference a ClusterIssuer which is +non-namespaced. .. _`Subject Alternative Names`: https://en.wikipedia.org/wiki/Subject_Alternative_Name @@ -54,3 +55,63 @@ Certificate can alternatively reference a ClusterIssuer which is non-namespaced. :hidden: certificates/issuer-specific-config/acme + +*************************************** +Certificate Duration and Renewal Window +*************************************** + +cert-manager Certificate resources also support custom validity durations and +renewal windows. + +**Important**: The backend service implementation can choose to generate a +certificate with a different validity period than what is requested in the +issuer. + +Although the duration and renewal periods are specified on the Certificate +resources, the corresponding Issuer or ClusterIssuer must support this. + +The table below shows the support state of the different backend services used +by issuer types: + +=========== ============================================================ +Issuer Description +=========== ============================================================ +ACME Only 'renewBefore' supported +CA Fully supported +Vault Fully supported (although the requested duration must be lower + than the configured Vault role's TTL) +Self Signed Fully supported +=========== ============================================================ + +The default duration for all certificates is 90 days and the default renewal +windows is 30 days. This means that certificates are considered valid for 3 +months and renewal will be attempted within 1 month of expiration. + +The *duration* and *renewBefore* parameters must be given in the golang `parseDuration string format `__. + +Example Usage +============= +Here an example of an issuer specifying the duration and renewal window. + +The certificate from the previous section is extended with a validity period of +24 hours and to begin trying to renew 12 hours before the certificate +expiration. + + .. code-block:: yaml + :linenos: + :emphasize-lines: 7,8 + + apiVersion: certmanager.k8s.io/v1alpha1 + kind: Certificate + metadata: + name: example + spec: + secretName: example-tls + duration: 24h + renewBefore: 12h + dnsNames: + - foo.example.com + - bar.example.com + issuerRef: + name: my-internal-ca + kind: Issuer diff --git a/pkg/apis/certmanager/v1alpha1/BUILD.bazel b/pkg/apis/certmanager/v1alpha1/BUILD.bazel index 8985b5af9..61e697a9d 100644 --- a/pkg/apis/certmanager/v1alpha1/BUILD.bazel +++ b/pkg/apis/certmanager/v1alpha1/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ + "const.go", "conversion.go", "defaults.go", "doc.go", diff --git a/pkg/apis/certmanager/v1alpha1/const.go b/pkg/apis/certmanager/v1alpha1/const.go new file mode 100644 index 000000000..ea295c345 --- /dev/null +++ b/pkg/apis/certmanager/v1alpha1/const.go @@ -0,0 +1,33 @@ +/* +Copyright 2018 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 v1alpha1 + +import "time" + +const ( + // minimum permitted certificate duration by cert-manager + MinimumCertificateDuration = time.Hour + + // default certificate duration if Issuer.spec.duration is not set + DefaultCertificateDuration = time.Hour * 24 * 90 + + // minimum certificate duration before certificate expiration + MinimumRenewBefore = time.Minute * 5 + + // Default duration before certificate expiration if Issuer.spec.renewBefore is not set + DefaultRenewBefore = time.Hour * 24 * 30 +) diff --git a/pkg/apis/certmanager/v1alpha1/types_certificate.go b/pkg/apis/certmanager/v1alpha1/types_certificate.go index 67e5fb057..f096f3928 100644 --- a/pkg/apis/certmanager/v1alpha1/types_certificate.go +++ b/pkg/apis/certmanager/v1alpha1/types_certificate.go @@ -57,6 +57,12 @@ type CertificateSpec struct { // Organization is the organization to be used on the Certificate Organization []string `json:"organization,omitempty"` + // Certificate default Duration + Duration *metav1.Duration `json:"duration,omitempty"` + + // Certificate renew before expiration duration + RenewBefore *metav1.Duration `json:"renewBefore,omitempty"` + // DNSNames is a list of subject alt names to be used on the Certificate DNSNames []string `json:"dnsNames,omitempty"` diff --git a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go index 4a26fc512..919af8134 100644 --- a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go @@ -468,6 +468,24 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + if *in == nil { + *out = nil + } else { + *out = new(v1.Duration) + (*in).DeepCopyInto(*out) + } + } + if in.RenewBefore != nil { + in, out := &in.RenewBefore, &out.RenewBefore + if *in == nil { + *out = nil + } else { + *out = new(v1.Duration) + (*in).DeepCopyInto(*out) + } + } if in.DNSNames != nil { in, out := &in.DNSNames, &out.DNSNames *out = make([]string, len(*in)) diff --git a/pkg/apis/certmanager/validation/BUILD.bazel b/pkg/apis/certmanager/validation/BUILD.bazel index 415e3ed9d..c19cde07f 100644 --- a/pkg/apis/certmanager/validation/BUILD.bazel +++ b/pkg/apis/certmanager/validation/BUILD.bazel @@ -32,6 +32,7 @@ go_test( "//pkg/issuer/acme/dns/rfc2136:go_default_library", "//test/util/generate:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", ], ) diff --git a/pkg/apis/certmanager/validation/certificate.go b/pkg/apis/certmanager/validation/certificate.go index e2ec69b2c..eeb638d33 100644 --- a/pkg/apis/certmanager/validation/certificate.go +++ b/pkg/apis/certmanager/validation/certificate.go @@ -70,6 +70,10 @@ func ValidateCertificateSpec(crt *v1alpha1.CertificateSpec, fldPath *field.Path) el = append(el, field.Invalid(fldPath.Child("keyAlgorithm"), crt.KeyAlgorithm, "must be either empty or one of rsa or ecdsa")) } + if crt.Duration != nil || crt.RenewBefore != nil { + el = append(el, ValidateDuration(crt, fldPath)...) + } + return el } @@ -148,3 +152,26 @@ func ValidateHTTP01SolverConfig(a *v1alpha1.HTTP01SolverConfig, fldPath *field.P // TODO: ensure 'ingress' is a valid resource name (i.e. DNS name) return el } + +func ValidateDuration(crt *v1alpha1.CertificateSpec, fldPath *field.Path) field.ErrorList { + el := field.ErrorList{} + + duration := v1alpha1.DefaultCertificateDuration + if crt.Duration != nil { + duration = crt.Duration.Duration + } + renewBefore := v1alpha1.DefaultRenewBefore + if crt.RenewBefore != nil { + renewBefore = crt.RenewBefore.Duration + } + if duration < v1alpha1.MinimumCertificateDuration { + el = append(el, field.Invalid(fldPath.Child("duration"), duration, fmt.Sprintf("certificate duration must be greater than %s", v1alpha1.MinimumCertificateDuration))) + } + if renewBefore < v1alpha1.MinimumRenewBefore { + el = append(el, field.Invalid(fldPath.Child("renewBefore"), renewBefore, fmt.Sprintf("certificate renewBefore must be greater than %s", v1alpha1.MinimumRenewBefore))) + } + if duration <= renewBefore { + el = append(el, field.Invalid(fldPath.Child("renewBefore"), renewBefore, fmt.Sprintf("certificate duration %s must be greater than renewBefore %s", duration, renewBefore))) + } + return el +} diff --git a/pkg/apis/certmanager/validation/certificate_for_issuer.go b/pkg/apis/certmanager/validation/certificate_for_issuer.go index 3d6bb48e2..098a3e806 100644 --- a/pkg/apis/certmanager/validation/certificate_for_issuer.go +++ b/pkg/apis/certmanager/validation/certificate_for_issuer.go @@ -59,6 +59,10 @@ func ValidateCertificateForACMEIssuer(crt *v1alpha1.CertificateSpec, issuer *v1a el = append(el, field.Invalid(specPath.Child("organization"), crt.Organization, "ACME does not support setting the organization name")) } + if crt.Duration != nil { + el = append(el, field.Invalid(specPath.Child("duration"), crt.Duration, "ACME does not support certificate durations")) + } + return el } diff --git a/pkg/apis/certmanager/validation/certificate_for_issuer_test.go b/pkg/apis/certmanager/validation/certificate_for_issuer_test.go index dc924b483..e7cd036ac 100644 --- a/pkg/apis/certmanager/validation/certificate_for_issuer_test.go +++ b/pkg/apis/certmanager/validation/certificate_for_issuer_test.go @@ -19,10 +19,13 @@ package validation import ( "reflect" "testing" + "time" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack/cert-manager/test/util/generate" "k8s.io/apimachinery/pkg/util/validation/field" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -130,6 +133,54 @@ func TestValidateCertificateForIssuer(t *testing.T) { field.Invalid(fldPath.Child("organization"), []string{"shouldfailorg"}, "ACME does not support setting the organization name"), }, }, + "acme certificate with duration set": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + Duration: &metav1.Duration{Duration: time.Minute * 60}, + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.DomainSolverConfig{ + { + Domains: []string{"example.com"}, + SolverConfig: v1alpha1.SolverConfig{ + HTTP01: &v1alpha1.HTTP01SolverConfig{}, + }, + }, + }, + }, + }, + }, + issuer: generate.Issuer(generate.IssuerConfig{ + Name: defaultTestIssuerName, + Namespace: defaultTestNamespace, + }), + errs: []*field.Error{ + field.Invalid(fldPath.Child("duration"), &metav1.Duration{Duration: time.Minute * 60}, "ACME does not support certificate durations"), + }, + }, + "acme certificate with renewBefore set": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + RenewBefore: &metav1.Duration{Duration: time.Minute * 60}, + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.DomainSolverConfig{ + { + Domains: []string{"example.com"}, + SolverConfig: v1alpha1.SolverConfig{ + HTTP01: &v1alpha1.HTTP01SolverConfig{}, + }, + }, + }, + }, + }, + }, + issuer: generate.Issuer(generate.IssuerConfig{ + Name: defaultTestIssuerName, + Namespace: defaultTestNamespace, + }), + errs: []*field.Error{}, + }, "certificate with unspecified issuer type": { crt: &v1alpha1.Certificate{ Spec: v1alpha1.CertificateSpec{ diff --git a/pkg/apis/certmanager/validation/certificate_test.go b/pkg/apis/certmanager/validation/certificate_test.go index 4c253b3c4..a033439b1 100644 --- a/pkg/apis/certmanager/validation/certificate_test.go +++ b/pkg/apis/certmanager/validation/certificate_test.go @@ -17,11 +17,15 @@ limitations under the License. package validation import ( + "fmt" "reflect" "testing" + "time" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "k8s.io/apimachinery/pkg/util/validation/field" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ( @@ -545,3 +549,135 @@ func TestValidateHTTP01SolverConfig(t *testing.T) { }) } } + +func TestValidateDuration(t *testing.T) { + usefulDurations := map[string]*metav1.Duration{ + "one second": {Duration: time.Second}, + "ten minutes": {Duration: time.Minute * 10}, + "half hour": {Duration: time.Minute * 30}, + "one hour": {Duration: time.Hour}, + "one month": {Duration: time.Hour * 24 * 30}, + "half year": {Duration: time.Hour * 24 * 180}, + "one year": {Duration: time.Hour * 24 * 365}, + "ten years": {Duration: time.Hour * 24 * 365 * 10}, + } + + fldPath := field.NewPath("spec") + scenarios := map[string]struct { + cfg *v1alpha1.Certificate + errs []*field.Error + }{ + "default duration and renewBefore": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + }, + "valid duration and renewBefore": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + Duration: usefulDurations["one year"], + RenewBefore: usefulDurations["half year"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + }, + "unset duration, valid renewBefore for default": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + RenewBefore: usefulDurations["one month"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + }, + "unset renewBefore, valid duration for default": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + Duration: usefulDurations["one year"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + }, + "renewBefore is bigger than the default duration": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + RenewBefore: usefulDurations["ten years"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + errs: []*field.Error{field.Invalid(fldPath.Child("renewBefore"), usefulDurations["ten years"].Duration, fmt.Sprintf("certificate duration %s must be greater than renewBefore %s", v1alpha1.DefaultCertificateDuration, usefulDurations["ten years"].Duration))}, + }, + "default renewBefore is bigger than the set duration": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + Duration: usefulDurations["one hour"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + errs: []*field.Error{field.Invalid(fldPath.Child("renewBefore"), v1alpha1.DefaultRenewBefore, fmt.Sprintf("certificate duration %s must be greater than renewBefore %s", usefulDurations["one hour"].Duration, v1alpha1.DefaultRenewBefore))}, + }, + "renewBefore is bigger than the duration": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + Duration: usefulDurations["one month"], + RenewBefore: usefulDurations["one year"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + errs: []*field.Error{field.Invalid(fldPath.Child("renewBefore"), usefulDurations["one year"].Duration, fmt.Sprintf("certificate duration %s must be greater than renewBefore %s", usefulDurations["one month"].Duration, usefulDurations["one year"].Duration))}, + }, + "renewBefore is less than the minimum permitted value": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + RenewBefore: usefulDurations["one second"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + errs: []*field.Error{field.Invalid(fldPath.Child("renewBefore"), usefulDurations["one second"].Duration, fmt.Sprintf("certificate renewBefore must be greater than %s", v1alpha1.MinimumRenewBefore))}, + }, + "duration is less than the minimum permitted value": { + cfg: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + Duration: usefulDurations["half hour"], + RenewBefore: usefulDurations["ten minutes"], + CommonName: "testcn", + SecretName: "abc", + IssuerRef: validIssuerRef, + }, + }, + errs: []*field.Error{field.Invalid(fldPath.Child("duration"), usefulDurations["half hour"].Duration, fmt.Sprintf("certificate duration must be greater than %s", v1alpha1.MinimumCertificateDuration))}, + }, + } + for n, s := range scenarios { + t.Run(n, func(t *testing.T) { + errs := ValidateDuration(&s.cfg.Spec, fldPath) + if len(errs) != len(s.errs) { + t.Errorf("Expected %v but got %v", s.errs, errs) + return + } + for i, e := range errs { + expectedErr := s.errs[i] + if !reflect.DeepEqual(e, expectedErr) { + t.Errorf("Expected %v but got %v", expectedErr, e) + } + } + }) + } +} diff --git a/pkg/apis/certmanager/validation/issuer.go b/pkg/apis/certmanager/validation/issuer.go index 4dd85c1ae..cc1cf27b0 100644 --- a/pkg/apis/certmanager/validation/issuer.go +++ b/pkg/apis/certmanager/validation/issuer.go @@ -80,6 +80,7 @@ func ValidateIssuerConfig(iss *v1alpha1.IssuerConfig, fldPath *field.Path) field if numConfigs == 0 { el = append(el, field.Required(fldPath, "at least one issuer must be configured")) } + return el } diff --git a/pkg/controller/BUILD.bazel b/pkg/controller/BUILD.bazel index e5a2fb2b7..c44e27903 100644 --- a/pkg/controller/BUILD.bazel +++ b/pkg/controller/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "//pkg/client/listers/certmanager/v1alpha1:go_default_library", "//pkg/issuer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/client-go/informers:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", diff --git a/pkg/controller/certificates/BUILD.bazel b/pkg/controller/certificates/BUILD.bazel index 6b0eda134..acb486f3b 100644 --- a/pkg/controller/certificates/BUILD.bazel +++ b/pkg/controller/certificates/BUILD.bazel @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", @@ -48,3 +48,13 @@ filegroup( tags = ["automanaged"], visibility = ["//visibility:public"], ) + +go_test( + name = "go_default_test", + srcs = ["sync_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/certmanager/v1alpha1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + ], +) diff --git a/pkg/controller/certificates/sync.go b/pkg/controller/certificates/sync.go index b23f86694..26857aca9 100644 --- a/pkg/controller/certificates/sync.go +++ b/pkg/controller/certificates/sync.go @@ -18,6 +18,7 @@ package certificates import ( "context" + "crypto/x509" "fmt" "reflect" "strings" @@ -69,6 +70,9 @@ var ( certificateGvk = v1alpha1.SchemeGroupVersion.WithKind("Certificate") ) +// to help testing +var now = time.Now + func (c *Controller) Sync(ctx context.Context, crt *v1alpha1.Certificate) (requeue bool, err error) { crtCopy := crt.DeepCopy() defer func() { @@ -180,7 +184,7 @@ func (c *Controller) Sync(ctx context.Context, crt *v1alpha1.Certificate) (reque } // check if the certificate needs renewal - needsRenew := c.Context.IssuerOptions.CertificateNeedsRenew(cert) + needsRenew := c.Context.IssuerOptions.CertificateNeedsRenew(cert, crt.Spec.RenewBefore) if needsRenew { return c.issue(ctx, i, crtCopy) } @@ -226,12 +230,11 @@ func (c *Controller) scheduleRenewal(crt *v1alpha1.Certificate) { return } - durationUntilExpiry := cert.NotAfter.Sub(time.Now()) - renewIn := durationUntilExpiry - c.Context.IssuerOptions.RenewBeforeExpiryDuration + renewIn := c.calculateDurationUntilRenew(cert, crt) c.scheduledWorkQueue.Add(key, renewIn) - glog.Infof("Certificate %s/%s scheduled for renewal in %d hours", crt.Namespace, crt.Name, renewIn/time.Hour) + glog.Infof("Certificate %s/%s scheduled for renewal in %s", crt.Namespace, crt.Name, renewIn.String()) } // issuerKind returns the kind of issuer for a certificate @@ -341,3 +344,43 @@ func (c *Controller) updateCertificateStatus(old, new *v1alpha1.Certificate) (*v // for CRDs (https://github.com/kubernetes/kubernetes/issues/38113) return c.CMClient.CertmanagerV1alpha1().Certificates(new.Namespace).Update(new) } + +// calculateDurationUntilRenew calculates how long cert-manager should wait to +// until attempting to renew this certificate resource. +func (c *Controller) calculateDurationUntilRenew(cert *x509.Certificate, crt *v1alpha1.Certificate) time.Duration { + messageCertificateDuration := "Certificate received from server has a validity duration of %s. The requested certificate validity duration was %s" + messageScheduleModified := "Certificate renewal duration was changed to fit inside the received certificate validity duration from issuer." + + // validate if the certificate received was with the issuer configured + // duration. If not we generate an event to warn the user of that fact. + certDuration := cert.NotAfter.Sub(cert.NotBefore) + if crt.Spec.Duration != nil && certDuration < crt.Spec.Duration.Duration { + s := fmt.Sprintf(messageCertificateDuration, certDuration, crt.Spec.Duration.Duration) + glog.Info(s) + // TODO Use the message as the reason in a 'renewal status' condition + } + + // renew is the duration before the certificate expiration that cert-manager + // will start to try renewing the certificate. + renewBefore := v1alpha1.DefaultRenewBefore + if crt.Spec.RenewBefore != nil { + renewBefore = crt.Spec.RenewBefore.Duration + } + + // Verify that the renewBefore duration is inside the certificate validity duration. + // If not we notify with an event that we will renew the certificate + // before (certificate duration / 3) of its expiration duration. + if renewBefore > certDuration { + glog.Info(messageScheduleModified) + // TODO Use the message as the reason in a 'renewal status' condition + // We will renew 1/3 before the expiration date. + renewBefore = certDuration / 3 + } + + // calculate the amount of time until expiry + durationUntilExpiry := cert.NotAfter.Sub(now()) + // calculate how long until we should start attempting to renew the certificate + renewIn := durationUntilExpiry - renewBefore + + return renewIn +} diff --git a/pkg/controller/certificates/sync_test.go b/pkg/controller/certificates/sync_test.go new file mode 100644 index 000000000..f1102f404 --- /dev/null +++ b/pkg/controller/certificates/sync_test.go @@ -0,0 +1,103 @@ +/* +Copyright 2018 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 ( + "crypto/x509" + "testing" + "time" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestCalculateDurationUntilRenew(t *testing.T) { + c := &Controller{} + currentTime := time.Now() + now = func() time.Time { return currentTime } + defer func() { now = time.Now }() + tests := []struct { + desc string + notBefore time.Time + notAfter time.Time + duration *metav1.Duration + renewBefore *metav1.Duration + expectedExpiry time.Duration + }{ + { + desc: "generate an event if certificate duration is lower than requested duration", + notBefore: now(), + notAfter: now().Add(time.Hour * 24 * 90), + duration: &metav1.Duration{time.Hour * 24 * 120}, + renewBefore: nil, + expectedExpiry: time.Hour * 24 * 60, + }, + { + desc: "default expiry to 30 days", + notBefore: now(), + notAfter: now().Add(time.Hour * 24 * 120), + duration: nil, + renewBefore: nil, + expectedExpiry: (time.Hour * 24 * 120) - (time.Hour * 24 * 30), + }, + { + desc: "default expiry to 2/3 of total duration if duration < 30 days", + notBefore: now(), + notAfter: now().Add(time.Hour * 24 * 20), + duration: nil, + renewBefore: nil, + expectedExpiry: time.Hour * 24 * 20 * 2 / 3, + }, + { + desc: "expiry of 2/3 of certificate duration when duration < 30 minutes", + notBefore: now(), + notAfter: now().Add(time.Hour), + duration: &metav1.Duration{time.Hour}, + renewBefore: &metav1.Duration{time.Hour / 3}, + expectedExpiry: time.Hour * 2 / 3, + }, + { + desc: "expiry of 60 days of certificate duration", + notBefore: now(), + notAfter: now().Add(time.Hour * 24 * 365), + duration: &metav1.Duration{time.Hour * 24 * 365}, + renewBefore: &metav1.Duration{time.Hour * 24 * 60}, + expectedExpiry: (time.Hour * 24 * 365) - (time.Hour * 24 * 60), + }, + { + desc: "expiry of 2/3 of certificate duration when renewBefore greater than certificate duration", + notBefore: now(), + notAfter: now().Add(time.Hour * 24 * 35), + duration: &metav1.Duration{time.Hour * 24 * 35}, + renewBefore: &metav1.Duration{time.Hour * 24 * 40}, + expectedExpiry: time.Hour * 24 * 35 * 2 / 3, + }, + } + for k, v := range tests { + cert := &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + Duration: v.duration, + RenewBefore: v.renewBefore, + }, + } + x509Cert := &x509.Certificate{NotBefore: v.notBefore, NotAfter: v.notAfter} + duration := c.calculateDurationUntilRenew(x509Cert, cert) + if duration != v.expectedExpiry { + t.Errorf("test # %d - %s: got %v, expected %v", k, v.desc, duration, v.expectedExpiry) + } + } +} diff --git a/pkg/controller/helper.go b/pkg/controller/helper.go index 4fc9a6198..cf30d8188 100644 --- a/pkg/controller/helper.go +++ b/pkg/controller/helper.go @@ -23,6 +23,7 @@ import ( cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type Helper interface { @@ -90,12 +91,17 @@ func (o IssuerOptions) CanUseAmbientCredentials(iss cmapi.GenericIssuer) bool { return false } -func (o IssuerOptions) CertificateNeedsRenew(cert *x509.Certificate) bool { +func (o IssuerOptions) CertificateNeedsRenew(cert *x509.Certificate, renewBefore *metav1.Duration) bool { + renewBeforeDuration := o.RenewBeforeExpiryDuration + if renewBefore != nil { + renewBeforeDuration = renewBefore.Duration + } + // calculate the amount of time until expiry durationUntilExpiry := cert.NotAfter.Sub(time.Now()) // calculate how long until we should start attempting to renew the // certificate - renewIn := durationUntilExpiry - o.RenewBeforeExpiryDuration + renewIn := durationUntilExpiry - renewBeforeDuration // if we should being attempting to renew now, then trigger a renewal if renewIn <= 0 { return true diff --git a/pkg/issuer/acme/issue.go b/pkg/issuer/acme/issue.go index dd2c25a6f..34c2cf390 100644 --- a/pkg/issuer/acme/issue.go +++ b/pkg/issuer/acme/issue.go @@ -182,7 +182,7 @@ func (a *Acme) Issue(ctx context.Context, crt *v1alpha1.Certificate) (issuer.Iss return a.retryOrder(crt, existingOrder) } - if a.Context.IssuerOptions.CertificateNeedsRenew(x509Cert) { + if a.Context.IssuerOptions.CertificateNeedsRenew(x509Cert, crt.Spec.RenewBefore) { // existing order's certificate is near expiry return a.retryOrder(crt, existingOrder) } diff --git a/pkg/issuer/vault/issue.go b/pkg/issuer/vault/issue.go index f15c2352e..df41536ff 100644 --- a/pkg/issuer/vault/issue.go +++ b/pkg/issuer/vault/issue.go @@ -47,8 +47,6 @@ const ( messageErrorIssueCert = "Error issuing TLS certificate: " messageCertIssued = "Certificate issued successfully" - - defaultCertificateDuration = time.Hour * 24 * 90 ) func (v *Vault) Issue(ctx context.Context, crt *v1alpha1.Certificate) (issuer.IssueResponse, error) { @@ -98,7 +96,13 @@ func (v *Vault) obtainCertificate(ctx context.Context, crt *v1alpha1.Certificate return nil, nil, nil, fmt.Errorf("error encoding certificate request: %s", err.Error()) } - crtBytes, caBytes, err := v.requestVaultCert(template.Subject.CommonName, template.DNSNames, pemRequestBuf.Bytes()) + certDuration := v1alpha1.DefaultCertificateDuration + if crt.Spec.Duration != nil { + certDuration = crt.Spec.Duration.Duration + } + + crtBytes, caBytes, err := v.requestVaultCert(template.Subject.CommonName, certDuration, template.DNSNames, pemRequestBuf.Bytes()) + if err != nil { return nil, nil, nil, err } @@ -212,7 +216,7 @@ func (v *Vault) requestTokenWithAppRoleRef(client *vault.Client, appRole *v1alph return token, nil } -func (v *Vault) requestVaultCert(commonName string, altNames []string, csr []byte) ([]byte, []byte, error) { +func (v *Vault) requestVaultCert(commonName string, certDuration time.Duration, altNames []string, csr []byte) ([]byte, []byte, error) { client, err := v.initVaultClient() if err != nil { return nil, nil, err @@ -223,7 +227,7 @@ func (v *Vault) requestVaultCert(commonName string, altNames []string, csr []byt parameters := map[string]string{ "common_name": commonName, "alt_names": strings.Join(altNames, ","), - "ttl": defaultCertificateDuration.String(), + "ttl": certDuration.String(), "csr": string(csr), "exclude_cn_from_sans": "true", } diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index 0c059df5b..063a15e4e 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -86,9 +86,6 @@ func OrganizationForCertificate(crt *v1alpha1.Certificate) []string { var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) -// default certification duration is 1 year -const defaultNotAfter = time.Hour * 24 * 365 - // GenerateCSR will generate a new *x509.CertificateRequest template to be used // by issuers that utilise CSRs to obtain Certificates. // The CSR will not be signed, and should be passed to either EncodeCSR or @@ -139,7 +136,13 @@ func GenerateTemplate(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) return nil, fmt.Errorf("failed to generate serial number: %s", err.Error()) } + certDuration := v1alpha1.DefaultCertificateDuration + if crt.Spec.Duration != nil { + certDuration = crt.Spec.Duration.Duration + } + pubKeyAlgo, _, err := SignatureAlgorithm(crt) + if err != nil { return nil, err } @@ -160,7 +163,7 @@ func GenerateTemplate(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate) CommonName: commonName, }, NotBefore: time.Now(), - NotAfter: time.Now().Add(defaultNotAfter), + NotAfter: time.Now().Add(certDuration), // see http://golang.org/pkg/crypto/x509/#KeyUsage KeyUsage: keyUsages, DNSNames: dnsNames, diff --git a/pkg/util/pki/generate_test.go b/pkg/util/pki/generate_test.go index 17b71889d..edc659b1e 100644 --- a/pkg/util/pki/generate_test.go +++ b/pkg/util/pki/generate_test.go @@ -240,7 +240,7 @@ func signTestCert(key crypto.Signer) *x509.Certificate { CommonName: commonName, }, NotBefore: time.Now(), - NotAfter: time.Now().Add(defaultNotAfter), + NotAfter: time.Now().Add(v1alpha1.DefaultCertificateDuration), // see http://golang.org/pkg/crypto/x509/#KeyUsage KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, } diff --git a/test/e2e/framework/BUILD.bazel b/test/e2e/framework/BUILD.bazel index 9f59e2da6..5a5cba75e 100644 --- a/test/e2e/framework/BUILD.bazel +++ b/test/e2e/framework/BUILD.bazel @@ -12,7 +12,9 @@ go_library( tags = ["manual"], visibility = ["//visibility:public"], deps = [ + "//pkg/apis/certmanager/v1alpha1:go_default_library", "//pkg/client/clientset/versioned:go_default_library", + "//pkg/util/pki:go_default_library", "//test/e2e/framework/addon:go_default_library", "//test/e2e/framework/config:go_default_library", "//test/e2e/framework/helper:go_default_library", diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index 8fcc70ce5..54df96817 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -17,15 +17,23 @@ limitations under the License. package framework import ( + "time" + "github.com/jetstack/cert-manager/test/e2e/framework/helper" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/pkg/util/pki" "k8s.io/api/core/v1" + api "k8s.io/api/core/v1" apiextcs "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" + "github.com/jetstack/cert-manager/test/e2e/framework/addon" "github.com/jetstack/cert-manager/test/e2e/framework/config" "github.com/jetstack/cert-manager/test/e2e/framework/util" @@ -168,6 +176,22 @@ func (f *Framework) Helper() *helper.Helper { } } +func (f *Framework) CertificateDurationValid(c *v1alpha1.Certificate, duration time.Duration) { + By("Verifying TLS certificate exists") + secret, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Get(c.Spec.SecretName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + certBytes, ok := secret.Data[api.TLSCertKey] + if !ok { + Failf("No certificate data found for Certificate %q", c.Name) + } + cert, err := pki.DecodeX509CertificateBytes(certBytes) + Expect(err).NotTo(HaveOccurred()) + By("Verifying that the duration is valid") + if cert.NotAfter.Sub(cert.NotBefore) != duration { + Failf("Expected duration of %s, got %s [NotBefore: %s, NotAfter: %s]", duration, cert.NotAfter.Sub(cert.NotBefore), cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339)) + } +} + // CertManagerDescribe is a wrapper function for ginkgo describe. Adds namespacing. func CertManagerDescribe(text string, body func()) bool { return Describe("[cert-manager] "+text, body) diff --git a/test/e2e/suite/issuers/acme/certificate/http01.go b/test/e2e/suite/issuers/acme/certificate/http01.go index f842bd938..4705494c0 100644 --- a/test/e2e/suite/issuers/acme/certificate/http01.go +++ b/test/e2e/suite/issuers/acme/certificate/http01.go @@ -116,7 +116,7 @@ var _ = framework.CertManagerDescribe("ACME Certificate (HTTP01)", func() { secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) By("Creating a Certificate") - _, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, acmeIngressClass, acmeIngressDomain)) + _, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil, acmeIngressClass, acmeIngressDomain)) Expect(err).NotTo(HaveOccurred()) By("Verifying the Certificate is valid") err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) @@ -130,7 +130,7 @@ var _ = framework.CertManagerDescribe("ACME Certificate (HTTP01)", func() { // the maximum length of a single segment of the domain being requested const maxLengthOfDomainSegment = 63 By("Creating a Certificate") - _, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, acmeIngressClass, fmt.Sprintf("%s.%s", cmutil.RandStringRunes(maxLengthOfDomainSegment), acmeIngressDomain))) + _, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil, acmeIngressClass, fmt.Sprintf("%s.%s", cmutil.RandStringRunes(maxLengthOfDomainSegment), acmeIngressDomain))) Expect(err).NotTo(HaveOccurred()) err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) Expect(err).NotTo(HaveOccurred()) @@ -141,7 +141,7 @@ var _ = framework.CertManagerDescribe("ACME Certificate (HTTP01)", func() { secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) By("Creating a Certificate") - _, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, acmeIngressClass, acmeIngressDomain, fmt.Sprintf("%s.%s", cmutil.RandStringRunes(5), acmeIngressDomain))) + _, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil, acmeIngressClass, acmeIngressDomain, fmt.Sprintf("%s.%s", cmutil.RandStringRunes(5), acmeIngressDomain))) Expect(err).NotTo(HaveOccurred()) By("Verifying the Certificate is valid") err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) @@ -153,7 +153,7 @@ var _ = framework.CertManagerDescribe("ACME Certificate (HTTP01)", func() { secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) By("Creating a Certificate") - cert, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, acmeIngressClass, acmeIngressDomain, fmt.Sprintf("%s.%s", cmutil.RandStringRunes(5), acmeIngressDomain))) + cert, err := certClient.Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil, acmeIngressClass, acmeIngressDomain, fmt.Sprintf("%s.%s", cmutil.RandStringRunes(5), acmeIngressDomain))) Expect(err).NotTo(HaveOccurred()) By("Verifying the Certificate is valid") err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) @@ -180,7 +180,7 @@ var _ = framework.CertManagerDescribe("ACME Certificate (HTTP01)", func() { Skip("Poorly designed test skipped until it can be rewritten") By("Creating a Certificate") - _, err := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name).Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, acmeIngressClass, "google.com")) + _, err := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name).Create(util.NewCertManagerACMECertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil, acmeIngressClass, "google.com")) Expect(err).NotTo(HaveOccurred()) By("Waiting for the Certificate to not have a ready condition") err = util.WaitForCertificateCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name), diff --git a/test/e2e/suite/issuers/ca/BUILD.bazel b/test/e2e/suite/issuers/ca/BUILD.bazel index 19a5fa3dd..2e7a0cead 100644 --- a/test/e2e/suite/issuers/ca/BUILD.bazel +++ b/test/e2e/suite/issuers/ca/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "//test/util:go_default_library", "//vendor/github.com/onsi/ginkgo:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", ], ) diff --git a/test/e2e/suite/issuers/ca/certificate.go b/test/e2e/suite/issuers/ca/certificate.go index 70c61921b..57e5a78c6 100644 --- a/test/e2e/suite/issuers/ca/certificate.go +++ b/test/e2e/suite/issuers/ca/certificate.go @@ -25,6 +25,7 @@ import ( "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack/cert-manager/test/e2e/framework" "github.com/jetstack/cert-manager/test/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = framework.CertManagerDescribe("CA Certificate", func() { @@ -64,7 +65,7 @@ var _ = framework.CertManagerDescribe("CA Certificate", func() { secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) By("Creating a Certificate") - _, err := certClient.Create(util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind)) + _, err := certClient.Create(util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil)) Expect(err).NotTo(HaveOccurred()) By("Verifying the Certificate is valid") err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Second*30) @@ -75,7 +76,7 @@ var _ = framework.CertManagerDescribe("CA Certificate", func() { certClient := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name) secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) - crt := util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind) + crt := util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil) crt.Spec.KeyAlgorithm = v1alpha1.ECDSAKeyAlgorithm crt.Spec.KeySize = 521 @@ -88,4 +89,39 @@ var _ = framework.CertManagerDescribe("CA Certificate", func() { Expect(err).NotTo(HaveOccurred()) }) + cases := []struct { + inputDuration *metav1.Duration + inputRenewBefore *metav1.Duration + expectedDuration time.Duration + label string + }{ + { + inputDuration: &metav1.Duration{time.Hour * 24 * 35}, + inputRenewBefore: nil, + expectedDuration: time.Hour * 24 * 35, + label: "35 days", + }, + { + inputDuration: nil, + inputRenewBefore: nil, + expectedDuration: time.Hour * 24 * 90, + label: "the default duration (90 days)", + }, + } + for _, v := range cases { + v := v + It("should generate a signed keypair valid for "+v.label, func() { + certClient := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name) + secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) + + By("Creating a Certificate") + cert, err := certClient.Create(util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, v.inputDuration, v.inputRenewBefore)) + Expect(err).NotTo(HaveOccurred()) + By("Verifying the Certificate is valid") + err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Second*30) + f.CertificateDurationValid(cert, v.expectedDuration) + Expect(err).NotTo(HaveOccurred()) + }) + } + }) diff --git a/test/e2e/suite/issuers/selfsigned/BUILD.bazel b/test/e2e/suite/issuers/selfsigned/BUILD.bazel index 9d2e593b8..c6262c0c0 100644 --- a/test/e2e/suite/issuers/selfsigned/BUILD.bazel +++ b/test/e2e/suite/issuers/selfsigned/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "//test/util:go_default_library", "//vendor/github.com/onsi/ginkgo:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", ], ) diff --git a/test/e2e/suite/issuers/selfsigned/certificate.go b/test/e2e/suite/issuers/selfsigned/certificate.go index 1da5091e8..bb1c3ce10 100644 --- a/test/e2e/suite/issuers/selfsigned/certificate.go +++ b/test/e2e/suite/issuers/selfsigned/certificate.go @@ -17,6 +17,7 @@ limitations under the License. package certificate import ( + "fmt" "time" . "github.com/onsi/ginkgo" @@ -25,6 +26,7 @@ import ( "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack/cert-manager/test/e2e/framework" "github.com/jetstack/cert-manager/test/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = framework.CertManagerDescribe("Self Signed Certificate", func() { @@ -51,9 +53,56 @@ var _ = framework.CertManagerDescribe("Self Signed Certificate", func() { }) Expect(err).NotTo(HaveOccurred()) By("Creating a Certificate") - _, err = certClient.Create(util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind)) + _, err = certClient.Create(util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil)) Expect(err).NotTo(HaveOccurred()) err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) Expect(err).NotTo(HaveOccurred()) }) + + cases := []struct { + inputDuration *metav1.Duration + inputRenewBefore *metav1.Duration + expectedDuration time.Duration + label string + }{ + { + inputDuration: &metav1.Duration{time.Hour * 24 * 35}, + inputRenewBefore: nil, + expectedDuration: time.Hour * 24 * 35, + label: "35 days", + }, + { + inputDuration: nil, + inputRenewBefore: nil, + expectedDuration: time.Hour * 24 * 90, + label: "the default duration (90 days)", + }, + } + for _, v := range cases { + v := v + It("should generate a signed keypair valid for "+v.label, func() { + certClient := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name) + secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) + + By("Creating an Issuer") + issuerDurationName := fmt.Sprintf("%s-%d", issuerName, v.expectedDuration) + _, err := f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerSelfSignedIssuer(issuerDurationName)) + Expect(err).NotTo(HaveOccurred()) + By("Waiting for Issuer to become Ready") + err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name), + issuerDurationName, + v1alpha1.IssuerCondition{ + Type: v1alpha1.IssuerConditionReady, + Status: v1alpha1.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Certificate") + cert, err := certClient.Create(util.NewCertManagerBasicCertificate(certificateName, certificateSecretName, issuerDurationName, v1alpha1.IssuerKind, v.inputDuration, v.inputRenewBefore)) + Expect(err).NotTo(HaveOccurred()) + err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Second*30) + f.CertificateDurationValid(cert, v.expectedDuration) + Expect(err).NotTo(HaveOccurred()) + }) + } }) diff --git a/test/e2e/suite/issuers/vault/certificate/BUILD.bazel b/test/e2e/suite/issuers/vault/certificate/BUILD.bazel index 46dc126ee..d8648691d 100644 --- a/test/e2e/suite/issuers/vault/certificate/BUILD.bazel +++ b/test/e2e/suite/issuers/vault/certificate/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "//test/util:go_default_library", "//vendor/github.com/onsi/ginkgo:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", ], ) diff --git a/test/e2e/suite/issuers/vault/certificate/approle.go b/test/e2e/suite/issuers/vault/certificate/approle.go index 7ace4d11f..20562eec9 100644 --- a/test/e2e/suite/issuers/vault/certificate/approle.go +++ b/test/e2e/suite/issuers/vault/certificate/approle.go @@ -28,6 +28,7 @@ import ( "github.com/jetstack/cert-manager/test/e2e/framework/addon/tiller" vaultaddon "github.com/jetstack/cert-manager/test/e2e/framework/addon/vault" "github.com/jetstack/cert-manager/test/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = framework.CertManagerDescribe("Vault Certificate (AppRole)", func() { @@ -98,6 +99,7 @@ var _ = framework.CertManagerDescribe("Vault Certificate (AppRole)", func() { secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) _, err := f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerVaultIssuerAppRole(issuerName, vaultURL, vaultPath, roleId, vaultSecretAppRoleName, authPath, vault.Details().VaultCA)) + Expect(err).NotTo(HaveOccurred()) By("Waiting for Issuer to become Ready") @@ -110,11 +112,75 @@ var _ = framework.CertManagerDescribe("Vault Certificate (AppRole)", func() { Expect(err).NotTo(HaveOccurred()) By("Creating a Certificate") - _, err = certClient.Create(util.NewCertManagerVaultCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind)) + _, err = certClient.Create(util.NewCertManagerVaultCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil)) Expect(err).NotTo(HaveOccurred()) err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) Expect(err).NotTo(HaveOccurred()) }) + + cases := []struct { + inputDuration *metav1.Duration + inputRenewBefore *metav1.Duration + expectedDuration time.Duration + label string + event string + }{ + { + inputDuration: &metav1.Duration{time.Hour * 24 * 35}, + inputRenewBefore: nil, + expectedDuration: time.Hour * 24 * 35, + label: "valid for 35 days", + }, + { + inputDuration: nil, + inputRenewBefore: nil, + expectedDuration: time.Hour * 24 * 90, + label: "valid for the default value (90 days)", + }, + { + inputDuration: &metav1.Duration{time.Hour * 24 * 365}, + inputRenewBefore: nil, + expectedDuration: time.Hour * 24 * 90, + label: "with Vault configured maximum TTL duration (90 days) when requested duration is greater than TTL", + }, + { + inputDuration: &metav1.Duration{time.Hour * 24 * 240}, + inputRenewBefore: &metav1.Duration{time.Hour * 24 * 120}, + expectedDuration: time.Hour * 24 * 90, + label: "with a warning event when renewBefore is bigger than the duration", + }, + } + + for _, v := range cases { + v := v + It("should generate a new certificate "+v.label, func() { + By("Creating an Issuer") + certClient := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name) + secretClient := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name) + + _, err := f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerVaultIssuerAppRole(issuerName, vault.Details().Host, vaultPath, roleId, vaultSecretAppRoleName, authPath, vault.Details().VaultCA)) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name), + issuerName, + v1alpha1.IssuerCondition{ + Type: v1alpha1.IssuerConditionReady, + Status: v1alpha1.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Certificate") + cert, err := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name).Create(util.NewCertManagerVaultCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, v.inputDuration, v.inputRenewBefore)) + Expect(err).NotTo(HaveOccurred()) + + err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) + + // Vault substract 30 seconds to the NotBefore date. + f.CertificateDurationValid(cert, v.expectedDuration+(30*time.Second)) + Expect(err).NotTo(HaveOccurred()) + }) + } }) diff --git a/test/e2e/suite/issuers/vault/certificate/approle_custom_mount.go b/test/e2e/suite/issuers/vault/certificate/approle_custom_mount.go index 1625550c3..d9474525c 100644 --- a/test/e2e/suite/issuers/vault/certificate/approle_custom_mount.go +++ b/test/e2e/suite/issuers/vault/certificate/approle_custom_mount.go @@ -111,7 +111,7 @@ var _ = framework.CertManagerDescribe("Vault Certificate (AppRole with a custom Expect(err).NotTo(HaveOccurred()) By("Creating a Certificate") - _, err = certClient.Create(util.NewCertManagerVaultCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind)) + _, err = certClient.Create(util.NewCertManagerVaultCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind, nil, nil)) Expect(err).NotTo(HaveOccurred()) err = util.WaitCertificateIssuedValid(certClient, secretClient, certificateName, time.Minute*5) diff --git a/test/e2e/suite/issuers/vault/issuer.go b/test/e2e/suite/issuers/vault/issuer.go index c12e1ba0f..daecf1805 100644 --- a/test/e2e/suite/issuers/vault/issuer.go +++ b/test/e2e/suite/issuers/vault/issuer.go @@ -94,7 +94,6 @@ var _ = framework.CertManagerDescribe("Vault Issuer", func() { _, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(vaultaddon.NewVaultAppRoleSecret(vaultSecretAppRoleName, secretId)) Expect(err).NotTo(HaveOccurred()) - By("Creating an Issuer") _, err = f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerVaultIssuerAppRole(issuerName, vault.Details().Host, vaultPath, roleId, vaultSecretAppRoleName, authPath, vault.Details().VaultCA)) Expect(err).NotTo(HaveOccurred()) diff --git a/test/util/generate/certificate.go b/test/util/generate/certificate.go index f901c181c..85a053100 100644 --- a/test/util/generate/certificate.go +++ b/test/util/generate/certificate.go @@ -31,6 +31,8 @@ type CertificateConfig struct { SecretName string CommonName string DNSNames []string + Duration *metav1.Duration + RenewBefore *metav1.Duration // ACME parameters SolverConfig v1alpha1.SolverConfig @@ -43,7 +45,9 @@ func Certificate(cfg CertificateConfig) *v1alpha1.Certificate { Namespace: cfg.Namespace, }, Spec: v1alpha1.CertificateSpec{ - SecretName: cfg.SecretName, + Duration: cfg.Duration, + RenewBefore: cfg.RenewBefore, + SecretName: cfg.SecretName, IssuerRef: v1alpha1.ObjectReference{ Name: cfg.IssuerName, Kind: cfg.IssuerKind, diff --git a/test/util/util.go b/test/util/util.go index e70b9857c..acfb5d950 100644 --- a/test/util/util.go +++ b/test/util/util.go @@ -350,7 +350,7 @@ func NewCertManagerCAClusterIssuer(name, secretName string) *v1alpha1.ClusterIss } } -func NewCertManagerBasicCertificate(name, secretName, issuerName string, issuerKind string) *v1alpha1.Certificate { +func NewCertManagerBasicCertificate(name, secretName, issuerName string, issuerKind string, duration, renewBefore *metav1.Duration) *v1alpha1.Certificate { return &v1alpha1.Certificate{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -359,6 +359,8 @@ func NewCertManagerBasicCertificate(name, secretName, issuerName string, issuerK CommonName: "test.domain.com", Organization: []string{"test-org"}, SecretName: secretName, + Duration: duration, + RenewBefore: renewBefore, IssuerRef: v1alpha1.ObjectReference{ Name: issuerName, Kind: issuerKind, @@ -367,15 +369,17 @@ func NewCertManagerBasicCertificate(name, secretName, issuerName string, issuerK } } -func NewCertManagerACMECertificate(name, secretName, issuerName string, issuerKind string, ingressClass string, cn string, dnsNames ...string) *v1alpha1.Certificate { +func NewCertManagerACMECertificate(name, secretName, issuerName string, issuerKind string, duration, renewBefore *metav1.Duration, ingressClass string, cn string, dnsNames ...string) *v1alpha1.Certificate { return &v1alpha1.Certificate{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, Spec: v1alpha1.CertificateSpec{ - CommonName: cn, - DNSNames: dnsNames, - SecretName: secretName, + CommonName: cn, + DNSNames: dnsNames, + SecretName: secretName, + Duration: duration, + RenewBefore: renewBefore, IssuerRef: v1alpha1.ObjectReference{ Name: issuerName, Kind: issuerKind, @@ -396,14 +400,16 @@ func NewCertManagerACMECertificate(name, secretName, issuerName string, issuerKi } } -func NewCertManagerVaultCertificate(name, secretName, issuerName string, issuerKind string) *v1alpha1.Certificate { +func NewCertManagerVaultCertificate(name, secretName, issuerName string, issuerKind string, duration, renewBefore *metav1.Duration) *v1alpha1.Certificate { return &v1alpha1.Certificate{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, Spec: v1alpha1.CertificateSpec{ - CommonName: "test.domain.com", - SecretName: secretName, + CommonName: "test.domain.com", + SecretName: secretName, + Duration: duration, + RenewBefore: renewBefore, IssuerRef: v1alpha1.ObjectReference{ Name: issuerName, Kind: issuerKind,