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:
commit
5b2d0d660e
@ -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"
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
56
pkg/controller/ingress-shim/helper.go
Normal file
56
pkg/controller/ingress-shim/helper.go
Normal 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
|
||||
}
|
||||
115
pkg/controller/ingress-shim/helper_test.go
Normal file
115
pkg/controller/ingress-shim/helper_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user