Merge pull request #3465 from wallrj/3396-ingress-renew-before

Add duration and renew-before Ingress annotations to set those fields on the Certificate
This commit is contained in:
jetstack-bot 2020-12-16 15:50:04 +00:00 committed by GitHub
commit 5b2d0d660e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 236 additions and 15 deletions

View File

@ -30,6 +30,12 @@ const (
// Annotation key for certificate common name.
CommonNameAnnotationKey = "cert-manager.io/common-name"
// Duration key for certificate duration.
DurationAnnotationKey = "cert-manager.io/duration"
// Annotation key for certificate renewBefore.
RenewBeforeAnnotationKey = "cert-manager.io/renew-before"
// Annotation key the 'name' of the Issuer resource.
IssuerNameAnnotationKey = "cert-manager.io/issuer-name"

View File

@ -5,6 +5,7 @@ go_library(
srcs = [
"checks.go",
"controller.go",
"helper.go",
"sync.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/controller/ingress-shim",
@ -36,7 +37,10 @@ go_library(
go_test(
name = "go_default_test",
srcs = ["sync_test.go"],
srcs = [
"helper_test.go",
"sync_test.go",
],
embed = [":go_default_library"],
deps = [
"//pkg/apis/acme/v1:go_default_library",
@ -44,6 +48,7 @@ go_test(
"//pkg/apis/meta/v1:go_default_library",
"//pkg/controller/test:go_default_library",
"//test/unit/gen:go_default_library",
"@com_github_stretchr_testify//assert:go_default_library",
"@io_k8s_api//networking/v1beta1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library",

View File

@ -0,0 +1,56 @@
/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"errors"
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
)
var (
errNilCertificate = errors.New("the supplied Certificate pointer was nil")
errInvalidIngressAnnotation = errors.New("invalid ingress annotation")
)
func translateIngressAnnotations(crt *cmapi.Certificate, annotations map[string]string) error {
if crt == nil {
return errNilCertificate
}
if commonName, found := annotations[cmapi.CommonNameAnnotationKey]; found {
crt.Spec.CommonName = commonName
}
if duration, found := annotations[cmapi.DurationAnnotationKey]; found {
duration, err := time.ParseDuration(duration)
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.DurationAnnotationKey, err)
}
crt.Spec.Duration = &metav1.Duration{Duration: duration}
}
if renewBefore, found := annotations[cmapi.RenewBeforeAnnotationKey]; found {
duration, err := time.ParseDuration(renewBefore)
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.RenewBeforeAnnotationKey, err)
}
crt.Spec.RenewBefore = &metav1.Duration{Duration: duration}
}
return nil
}

View File

@ -0,0 +1,115 @@
/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
"github.com/jetstack/cert-manager/test/unit/gen"
)
func TestTranslateIngressAnnotations(t *testing.T) {
type testCase struct {
crt *cmapi.Certificate
annotations map[string]string
mutate func(*testCase)
check func(*assert.Assertions, *cmapi.Certificate)
expectedError error
}
validAnnotations := func() map[string]string {
return map[string]string{
cmapi.CommonNameAnnotationKey: "www.example.com",
cmapi.DurationAnnotationKey: "168h", // 1 week
cmapi.RenewBeforeAnnotationKey: "24h",
}
}
tests := map[string]testCase{
"success": {
crt: gen.Certificate("example-cert"),
annotations: validAnnotations(),
check: func(a *assert.Assertions, crt *cmapi.Certificate) {
a.Equal("www.example.com", crt.Spec.CommonName)
a.Equal(&metav1.Duration{Duration: time.Hour * 24 * 7}, crt.Spec.Duration)
a.Equal(&metav1.Duration{Duration: time.Hour * 24}, crt.Spec.RenewBefore)
},
},
"nil annotations": {
crt: gen.Certificate("example-cert"),
annotations: nil,
},
"empty annotations": {
crt: gen.Certificate("example-cert"),
annotations: map[string]string{},
},
"nil certificate": {
crt: nil,
annotations: validAnnotations(),
expectedError: errNilCertificate,
},
"bad duration": {
crt: gen.Certificate("example-cert"),
annotations: validAnnotations(),
mutate: func(tc *testCase) {
tc.annotations[cmapi.DurationAnnotationKey] = "an un-parsable duration string"
},
expectedError: errInvalidIngressAnnotation,
},
"bad renewBefore": {
crt: gen.Certificate("example-cert"),
annotations: validAnnotations(),
mutate: func(tc *testCase) {
tc.annotations[cmapi.RenewBeforeAnnotationKey] = "an un-parsable duration string"
},
expectedError: errInvalidIngressAnnotation,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
if tc.mutate != nil {
tc.mutate(&tc)
}
crt := tc.crt.DeepCopy()
err := translateIngressAnnotations(crt, tc.annotations)
if tc.expectedError != nil {
assertErrorIs(t, err, tc.expectedError)
} else {
assert.NoError(t, err)
}
if tc.check != nil {
tc.check(assert.New(t), crt)
}
})
}
}
// assertErrorIs checks that the supplied error has the target error in its chain.
// TODO Upgrade to next release of testify package which has this built in.
func assertErrorIs(t *testing.T, err, target error) {
if assert.Error(t, err) {
assert.Truef(t, errors.Is(err, target), "unexpected error type. err: %v, target: %v", err, target)
}
}

View File

@ -156,13 +156,11 @@ func (c *controller) buildCertificates(ctx context.Context, ing *networkingv1bet
},
}
err = c.setIssuerSpecificConfig(crt, ing, tls)
if err != nil {
setIssuerSpecificConfig(crt, ing)
if err := translateIngressAnnotations(crt, ing.Annotations); err != nil {
return nil, nil, err
}
c.setCommonName(crt, ing)
// check if a Certificate for this TLS entry already exists, and if it
// does then skip this entry
if existingCrt != nil {
@ -188,10 +186,7 @@ func (c *controller) buildCertificates(ctx context.Context, ing *networkingv1bet
updateCrt.Spec = crt.Spec
updateCrt.Labels = crt.Labels
err = c.setIssuerSpecificConfig(updateCrt, ing, tls)
if err != nil {
return nil, nil, err
}
setIssuerSpecificConfig(updateCrt, ing)
updateCrts = append(updateCrts, updateCrt)
} else {
newCrts = append(newCrts, crt)
@ -273,7 +268,7 @@ func certNeedsUpdate(a, b *cmapi.Certificate) bool {
return false
}
func (c *controller) setIssuerSpecificConfig(crt *cmapi.Certificate, ing *networkingv1beta1.Ingress, tls networkingv1beta1.IngressTLS) error {
func setIssuerSpecificConfig(crt *cmapi.Certificate, ing *networkingv1beta1.Ingress) {
ingAnnotations := ing.Annotations
if ingAnnotations == nil {
ingAnnotations = map[string]string{}
@ -299,11 +294,9 @@ func (c *controller) setIssuerSpecificConfig(crt *cmapi.Certificate, ing *networ
}
crt.Annotations[cmacme.ACMECertificateHTTP01IngressClassOverride] = ingressClassVal
}
return nil
}
func (c *controller) setCommonName(crt *cmapi.Certificate, ing *networkingv1beta1.Ingress) {
func setCommonName(crt *cmapi.Certificate, ing *networkingv1beta1.Ingress) {
// if annotation is set use that as CN
if ing.Annotations != nil && ing.Annotations[cmapi.CommonNameAnnotationKey] != "" {
crt.Spec.CommonName = ing.Annotations[cmapi.CommonNameAnnotationKey]

View File

@ -960,6 +960,32 @@ func TestSync(t *testing.T) {
},
},
},
{
Name: "Failure to translateIngressAnnotations",
Issuer: acmeIssuer,
Ingress: &networkingv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-name",
Namespace: gen.DefaultTestNamespace,
Annotations: map[string]string{
cmapi.IngressIssuerNameAnnotationKey: "issuer-name",
cmapi.IssuerKindAnnotationKey: "Issuer",
cmapi.IssuerGroupAnnotationKey: "cert-manager.io",
cmapi.RenewBeforeAnnotationKey: "invalid renew before value",
},
UID: types.UID("ingress-name"),
},
Spec: networkingv1beta1.IngressSpec{
TLS: []networkingv1beta1.IngressTLS{
{
Hosts: []string{"example.com"},
SecretName: "example-com-tls",
},
},
},
},
Err: true,
},
}
testFn := func(test testT) func(t *testing.T) {
return func(t *testing.T) {

View File

@ -522,19 +522,23 @@ func (s *Suite) Define() {
Expect(err).NotTo(HaveOccurred())
}, featureset.OnlySAN)
s.it(f, "should issue a basic certificate for a single commonName and distinct dnsName defined by an ingress with annotations", func(issuerRef cmmeta.ObjectReference) {
s.it(f, "should issue a basic certificate defined by an ingress with certificate field annotations", func(issuerRef cmmeta.ObjectReference) {
ingClient := f.KubeClientSet.NetworkingV1beta1().Ingresses(f.Namespace.Name)
name := "testcert-ingress"
secretName := "testcert-ingress-tls"
domain := s.newDomain()
duration := time.Hour * 999
renewBefore := time.Hour * 111
By("Creating an Ingress with the issuer name annotation set")
By("Creating an Ingress with annotations for issuerRef and other Certificate fields")
ingress, err := ingClient.Create(context.TODO(), e2eutil.NewIngress(name, secretName, map[string]string{
"cert-manager.io/issuer": issuerRef.Name,
"cert-manager.io/issuer-kind": issuerRef.Kind,
"cert-manager.io/issuer-group": issuerRef.Group,
"cert-manager.io/common-name": domain,
"cert-manager.io/duration": duration.String(),
"cert-manager.io/renew-before": renewBefore.String(),
}, domain), metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
@ -549,6 +553,22 @@ func (s *Suite) Define() {
err = f.Helper().WaitCertificateIssued(f.Namespace.Name, certName, time.Minute*5)
Expect(err).NotTo(HaveOccurred())
// Verify that the ingres-shim has translated all the supplied
// annotations into equivalent Certificate field values
By("Validating the created Certificate")
err = f.Helper().ValidateCertificate(
f.Namespace.Name, certName,
func(certificate *cmapi.Certificate, _ *corev1.Secret) error {
Expect(certificate.Spec.DNSNames).To(ConsistOf(domain))
Expect(certificate.Spec.CommonName).To(Equal(domain))
Expect(certificate.Spec.Duration.Duration).To(Equal(duration))
Expect(certificate.Spec.RenewBefore.Duration).To(Equal(renewBefore))
return nil
},
)
// Verify that the issuer has preserved all the Certificate values
// in the signed certificate
By("Validating the issued Certificate...")
err = f.Helper().ValidateCertificate(f.Namespace.Name, certName, f.Helper().ValidationSetForUnsupportedFeatureSet(s.UnsupportedFeatures)...)
Expect(err).NotTo(HaveOccurred())