diff --git a/pkg/apis/certmanager/v1/types.go b/pkg/apis/certmanager/v1/types.go index 276722793e9..31e737c60fa 100644 --- a/pkg/apis/certmanager/v1/types.go +++ b/pkg/apis/certmanager/v1/types.go @@ -145,6 +145,10 @@ const ( // controller only processes Ingresses with this annotation either unset, or // set to either the configured value or the empty string. IngressClassAnnotationKey = "kubernetes.io/ingress.class" + + // IngressSecretTemplate can be used to set the secretTemplate field in the generated Certificate. + // The value is a JSON representation of secretTemplate and must not have any unknown fields. + IngressSecretTemplate = "cert-manager.io/secret-template" ) // Annotation names for CertificateRequests diff --git a/pkg/controller/certificate-shim/helper.go b/pkg/controller/certificate-shim/helper.go index 198bee84e9c..ce6f59152e0 100644 --- a/pkg/controller/certificate-shim/helper.go +++ b/pkg/controller/certificate-shim/helper.go @@ -17,6 +17,7 @@ limitations under the License. package shimhelper import ( + "encoding/json" "errors" "fmt" "reflect" @@ -268,5 +269,23 @@ func translateAnnotations(crt *cmapi.Certificate, ingLikeAnnotations map[string] } } + if secretTemplateJson, found := ingLikeAnnotations[cmapi.IngressSecretTemplate]; found { + decoder := json.NewDecoder(strings.NewReader(secretTemplateJson)) + decoder.DisallowUnknownFields() + + var secretTemplate = new(cmapi.CertificateSecretTemplate) + if err := decoder.Decode(secretTemplate); err != nil { + return fmt.Errorf("%w %q: error parsing secret template JSON: %v", errInvalidIngressAnnotation, cmapi.IngressSecretTemplate, err) + } + for annotationKey := range secretTemplate.Annotations { + if strings.HasPrefix(annotationKey, "cert-manager.io/") { + return fmt.Errorf("%w %q: secretTemplate must not have cert-manager.io/ annotations: %q", errInvalidIngressAnnotation, cmapi.IngressSecretTemplate, annotationKey) + } + } + if len(secretTemplate.Annotations) > 0 || len(secretTemplate.Labels) > 0 { + crt.Spec.SecretTemplate = secretTemplate + } + } + return nil } diff --git a/pkg/controller/certificate-shim/sync_test.go b/pkg/controller/certificate-shim/sync_test.go index 4914d5b0e33..bd98f36267c 100644 --- a/pkg/controller/certificate-shim/sync_test.go +++ b/pkg/controller/certificate-shim/sync_test.go @@ -536,6 +536,105 @@ func TestSync(t *testing.T) { }, }, }, + { + Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and valid secret template annotation", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressSecretTemplate: `{ "annotations": { "example-annotation" : "dummy-value" }, "labels": { "example-label" : "dummy-value" } }`, + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1.IngressSpec{ + TLS: []networkingv1.IngressTLS{ + { + Hosts: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + }, + }, + }, + }, + ClusterIssuerLister: []runtime.Object{acmeClusterIssuer}, + ExpectedEvents: []string{`Normal CreateCertificate Successfully created Certificate "example-com-tls"`}, + ExpectedCreate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com-tls", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + SecretTemplate: &cmapi.CertificateSecretTemplate{ + Annotations: map[string]string{ + "example-annotation": "dummy-value", + }, + Labels: map[string]string{ + "example-label": "dummy-value", + }, + }, + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "secret template annotation should not allow cert-manager.io/ annotations", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressSecretTemplate: `{ "annotations": { "cert-manager.io/disallowed-annotation" : "dummy-value" } }`, + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1.IngressSpec{ + TLS: []networkingv1.IngressTLS{ + { + Hosts: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + }, + }, + }, + }, + Err: true, + }, + { + Name: "secret template annotation should not allow unknown fields", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressSecretTemplate: `{ "unknown-field": "true" }`, + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1.IngressSpec{ + TLS: []networkingv1.IngressTLS{ + { + Hosts: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + }, + }, + }, + }, + Err: true, + }, { Name: "edit-in-place set to false should not trigger editing the ingress in-place", Issuer: acmeClusterIssuer,