diff --git a/LICENSES b/LICENSES index 3e44c3df8..318d96b9c 100644 --- a/LICENSES +++ b/LICENSES @@ -16651,35 +16651,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -================================================================================ -= vendor/github.com/shurcooL/sanitized_anchor_name licensed under: = - -MIT License - -Copyright (c) 2015 Dmitri Shuralyov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -= vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE c670c44b8d826e9b7b99077e5c7ba283 -================================================================================ - - ================================================================================ = vendor/github.com/sirupsen/logrus licensed under: = @@ -24285,6 +24256,215 @@ third-party archives. ================================================================================ +================================================================================ += vendor/sigs.k8s.io/gateway-api licensed under: = + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 The Kubernetes 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. + += vendor/sigs.k8s.io/gateway-api/LICENSE 2a0fcf36e548ee863cd9e49f96dcd666 +================================================================================ + + ================================================================================ = vendor/sigs.k8s.io/kustomize/api licensed under: = diff --git a/cmd/controller/app/BUILD.bazel b/cmd/controller/app/BUILD.bazel index 302bcd8a6..b9268ec43 100644 --- a/cmd/controller/app/BUILD.bazel +++ b/cmd/controller/app/BUILD.bazel @@ -17,9 +17,10 @@ go_library( "//pkg/controller:go_default_library", "//pkg/controller/acmechallenges:go_default_library", "//pkg/controller/acmeorders:go_default_library", + "//pkg/controller/certificate-shim/gateways:go_default_library", + "//pkg/controller/certificate-shim/ingresses:go_default_library", "//pkg/controller/certificates/trigger:go_default_library", "//pkg/controller/clusterissuers:go_default_library", - "//pkg/controller/ingress-shim:go_default_library", "//pkg/controller/issuers:go_default_library", "//pkg/issuer/acme:go_default_library", "//pkg/issuer/acme/dns/util:go_default_library", @@ -45,6 +46,10 @@ go_library( "@io_k8s_client_go//tools/leaderelection:go_default_library", "@io_k8s_client_go//tools/leaderelection/resourcelock:go_default_library", "@io_k8s_client_go//tools/record:go_default_library", + "@io_k8s_sigs_gateway_api//apis/v1alpha1:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/clientset/versioned:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/clientset/versioned/scheme:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/informers/externalversions:go_default_library", "@io_k8s_utils//clock:go_default_library", ], ) diff --git a/cmd/controller/app/controller.go b/cmd/controller/app/controller.go index 9a2ed51e5..cf7facfea 100644 --- a/cmd/controller/app/controller.go +++ b/cmd/controller/app/controller.go @@ -35,6 +35,10 @@ import ( "k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/client-go/tools/record" "k8s.io/utils/clock" + gwapi "sigs.k8s.io/gateway-api/apis/v1alpha1" + gwclient "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" + gwscheme "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/scheme" + gwinformers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions" "github.com/jetstack/cert-manager/cmd/controller/app/options" "github.com/jetstack/cert-manager/pkg/acme/accounts" @@ -116,6 +120,7 @@ func Run(opts *options.ControllerOptions, stopCh <-chan struct{}) { log.V(logf.DebugLevel).Info("starting shared informer factories") ctx.SharedInformerFactory.Start(stopCh) ctx.KubeSharedInformerFactory.Start(stopCh) + ctx.GWShared.Start(stopCh) wg.Wait() log.V(logf.InfoLevel).Info("control loops exited") ctx.Metrics.Shutdown(metricsServer) @@ -163,6 +168,24 @@ func buildControllerContext(ctx context.Context, stopCh <-chan struct{}, opts *o return nil, nil, fmt.Errorf("error creating kubernetes client: %s", err.Error()) } + // The user may have enabled the gateway-shim controller but forgotten to + // install the Gateway API CRDs. Failing here will cause cert-manager to go + // into CrashLoopBackoff which is nice and obvious. + d := cl.Discovery() + resources, err := d.ServerResourcesForGroupVersion(gwapi.GroupVersion.String()) + if err != nil { + return nil, nil, fmt.Errorf("couldn't discover Gateway API resources (are the Gateway API CRDs installed?): %w", err) + } + if len(resources.APIResources) == 0 { + return nil, nil, fmt.Errorf("no gateway API resources were discovered (are the Gateway API CRDs installed?)") + } + + // Create a GatewayAPI client + gwcl, err := gwclient.NewForConfig(kubeCfg) + if err != nil { + return nil, nil, fmt.Errorf("error creating kubernetes client: %s", err.Error()) + } + nameservers := opts.DNS01RecursiveNameservers if len(nameservers) == 0 { nameservers = dnsutil.RecursiveNameservers @@ -193,6 +216,7 @@ func buildControllerContext(ctx context.Context, stopCh <-chan struct{}, opts *o // Add cert-manager types to the default Kubernetes Scheme so Events can be // logged properly intscheme.AddToScheme(scheme.Scheme) + gwscheme.AddToScheme(scheme.Scheme) log.V(logf.DebugLevel).Info("creating event broadcaster") eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(logf.WithInfof(log.V(logf.DebugLevel)).Infof) @@ -201,6 +225,7 @@ func buildControllerContext(ctx context.Context, stopCh <-chan struct{}, opts *o sharedInformerFactory := informers.NewSharedInformerFactoryWithOptions(intcl, resyncPeriod, informers.WithNamespace(opts.Namespace)) kubeSharedInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(cl, resyncPeriod, kubeinformers.WithNamespace(opts.Namespace)) + gwSharedInformerFactory := gwinformers.NewSharedInformerFactoryWithOptions(gwcl, resyncPeriod, gwinformers.WithNamespace(opts.Namespace)) acmeAccountRegistry := accounts.NewDefaultRegistry() @@ -210,9 +235,11 @@ func buildControllerContext(ctx context.Context, stopCh <-chan struct{}, opts *o RESTConfig: kubeCfg, Client: cl, CMClient: intcl, + GWClient: gwcl, Recorder: recorder, KubeSharedInformerFactory: kubeSharedInformerFactory, SharedInformerFactory: sharedInformerFactory, + GWShared: gwSharedInformerFactory, Namespace: opts.Namespace, Clock: clock.RealClock{}, Metrics: metrics.New(log, clock.RealClock{}), diff --git a/cmd/controller/app/options/BUILD.bazel b/cmd/controller/app/options/BUILD.bazel index a2dd50d12..b2ef44e15 100644 --- a/cmd/controller/app/options/BUILD.bazel +++ b/cmd/controller/app/options/BUILD.bazel @@ -9,6 +9,8 @@ go_library( "//pkg/apis/certmanager:go_default_library", "//pkg/controller/acmechallenges:go_default_library", "//pkg/controller/acmeorders:go_default_library", + "//pkg/controller/certificate-shim/gateways:go_default_library", + "//pkg/controller/certificate-shim/ingresses:go_default_library", "//pkg/controller/certificaterequests/acme:go_default_library", "//pkg/controller/certificaterequests/approver:go_default_library", "//pkg/controller/certificaterequests/ca:go_default_library", @@ -26,7 +28,6 @@ go_library( "//pkg/controller/certificatesigningrequests/selfsigned:go_default_library", "//pkg/controller/certificatesigningrequests/vault:go_default_library", "//pkg/controller/clusterissuers:go_default_library", - "//pkg/controller/ingress-shim:go_default_library", "//pkg/controller/issuers:go_default_library", "//pkg/feature:go_default_library", "//pkg/logs:go_default_library", diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go index b416ea1ac..75297f4f0 100644 --- a/cmd/controller/app/options/options.go +++ b/cmd/controller/app/options/options.go @@ -28,6 +28,8 @@ import ( cm "github.com/jetstack/cert-manager/pkg/apis/certmanager" challengescontroller "github.com/jetstack/cert-manager/pkg/controller/acmechallenges" orderscontroller "github.com/jetstack/cert-manager/pkg/controller/acmeorders" + shimgatewaycontroller "github.com/jetstack/cert-manager/pkg/controller/certificate-shim/gateways" + shimingresscontroller "github.com/jetstack/cert-manager/pkg/controller/certificate-shim/ingresses" cracmecontroller "github.com/jetstack/cert-manager/pkg/controller/certificaterequests/acme" crapprovercontroller "github.com/jetstack/cert-manager/pkg/controller/certificaterequests/approver" crcacontroller "github.com/jetstack/cert-manager/pkg/controller/certificaterequests/ca" @@ -45,7 +47,6 @@ import ( csrselfsignedcontroller "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/selfsigned" csrvaultcontroller "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/vault" clusterissuerscontroller "github.com/jetstack/cert-manager/pkg/controller/clusterissuers" - ingressshimcontroller "github.com/jetstack/cert-manager/pkg/controller/ingress-shim" issuerscontroller "github.com/jetstack/cert-manager/pkg/controller/issuers" "github.com/jetstack/cert-manager/pkg/feature" logf "github.com/jetstack/cert-manager/pkg/logs" @@ -150,7 +151,8 @@ var ( issuerscontroller.ControllerName, clusterissuerscontroller.ControllerName, certificatesmetricscontroller.ControllerName, - ingressshimcontroller.ControllerName, + shimingresscontroller.ControllerName, + shimgatewaycontroller.ControllerName, orderscontroller.ControllerName, challengescontroller.ControllerName, cracmecontroller.CRControllerName, @@ -172,7 +174,7 @@ var ( issuerscontroller.ControllerName, clusterissuerscontroller.ControllerName, certificatesmetricscontroller.ControllerName, - ingressshimcontroller.ControllerName, + shimingresscontroller.ControllerName, orderscontroller.ControllerName, challengescontroller.ControllerName, cracmecontroller.CRControllerName, diff --git a/cmd/controller/app/start.go b/cmd/controller/app/start.go index 1d7572050..5693d28ab 100644 --- a/cmd/controller/app/start.go +++ b/cmd/controller/app/start.go @@ -26,9 +26,10 @@ import ( "github.com/jetstack/cert-manager/cmd/controller/app/options" _ "github.com/jetstack/cert-manager/pkg/controller/acmechallenges" _ "github.com/jetstack/cert-manager/pkg/controller/acmeorders" + _ "github.com/jetstack/cert-manager/pkg/controller/certificate-shim/gateways" + _ "github.com/jetstack/cert-manager/pkg/controller/certificate-shim/ingresses" _ "github.com/jetstack/cert-manager/pkg/controller/certificates/trigger" _ "github.com/jetstack/cert-manager/pkg/controller/clusterissuers" - _ "github.com/jetstack/cert-manager/pkg/controller/ingress-shim" _ "github.com/jetstack/cert-manager/pkg/controller/issuers" _ "github.com/jetstack/cert-manager/pkg/issuer/acme" _ "github.com/jetstack/cert-manager/pkg/issuer/ca" diff --git a/deploy/charts/cert-manager/templates/rbac.yaml b/deploy/charts/cert-manager/templates/rbac.yaml index bf6e6653e..fbf15192b 100644 --- a/deploy/charts/cert-manager/templates/rbac.yaml +++ b/deploy/charts/cert-manager/templates/rbac.yaml @@ -269,6 +269,12 @@ rules: - apiGroups: ["networking.k8s.io"] resources: ["ingresses/finalizers"] verbs: ["update"] + - apiGroups: ["networking.x-k8s.io"] + resources: ["gateways"] + verbs: ["get", "list", "watch"] + - apiGroups: ["networking.x-k8s.io"] + resources: ["gateways/finalizers"] + verbs: ["update"] - apiGroups: [""] resources: ["events"] verbs: ["create", "patch"] diff --git a/devel/addon/certmanager/install.sh b/devel/addon/certmanager/install.sh index 8745bf1d6..9caadce87 100755 --- a/devel/addon/certmanager/install.sh +++ b/devel/addon/certmanager/install.sh @@ -62,6 +62,6 @@ helm upgrade \ --set webhook.image.tag="${APP_VERSION}" \ --set installCRDs=true \ --set featureGates="${FEATURE_GATES:-}" \ - --set "extraArgs={--dns01-recursive-nameservers=${SERVICE_IP_PREFIX}.16:53,--dns01-recursive-nameservers-only=true}" \ + --set "extraArgs={--dns01-recursive-nameservers=${SERVICE_IP_PREFIX}.16:53,--dns01-recursive-nameservers-only=true,--controllers=*\,gateway-shim}" \ "$RELEASE_NAME" \ "$REPO_ROOT/bazel-bin/deploy/charts/cert-manager/cert-manager.tgz" diff --git a/go.mod b/go.mod index 92b2fa002..2c5e9c6fd 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( rsc.io/letsencrypt v0.0.3 // indirect sigs.k8s.io/controller-runtime v0.9.2 sigs.k8s.io/controller-tools v0.6.0 + sigs.k8s.io/gateway-api v0.3.0 sigs.k8s.io/yaml v1.2.0 software.sslmate.com/src/go-pkcs12 v0.0.0-20200830195227-52f69702a001 ) diff --git a/go.sum b/go.sum index 395168199..f21e9b1eb 100644 --- a/go.sum +++ b/go.sum @@ -33,9 +33,11 @@ github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= @@ -43,6 +45,7 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8K github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.2.0 h1:nQOZzFCudTh+TvquAtCRjM01VEYx85e9qbwt5ncW4L8= @@ -95,6 +98,7 @@ github.com/Venafi/vcert/v4 v4.13.1/go.mod h1:Z3sJFoAurFNXPpoSUSHq46aIeHLiGQEMDhp github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/ahmetb/gen-crd-api-reference-docs v0.2.1-0.20201224172655-df869c1245d4/go.mod h1:TdjdkYhlOifCQWPs1UdTma97kQQMozf5h26hTuG70u8= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0 h1:NNsy0ugDFZbu6S11NlsR4Kmko9hJ00gv9U2JHbwpf3g= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -266,6 +270,7 @@ github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZM github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -299,8 +304,10 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/zapr v0.2.0/go.mod h1:qhKdvif7YF5GI9NWEpyxTSSBdGmzkNguibrdCNVPunU= github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= @@ -534,6 +541,7 @@ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -612,10 +620,13 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7 github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= @@ -655,6 +666,7 @@ github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY7 github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -701,6 +713,7 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= @@ -709,6 +722,7 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= @@ -807,8 +821,9 @@ github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9 github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= @@ -821,7 +836,6 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -934,17 +948,21 @@ go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0H go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1011,6 +1029,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1047,9 +1066,11 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1125,6 +1146,7 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1136,6 +1158,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -1157,6 +1180,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= @@ -1253,6 +1277,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= @@ -1270,21 +1295,29 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8= k8s.io/api v0.21.0/go.mod h1:+YbrhBBGgsxbF6o6Kj4KJPJnBmAKuXDeS3E18bgHNVU= k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= k8s.io/api v0.21.2 h1:vz7DqmRsXTCSa6pNxXwQ1IYeAZgdIsua+DZU+o+SX3Y= k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo= +k8s.io/apiextensions-apiserver v0.20.1/go.mod h1:ntnrZV+6a3dB504qwC5PN/Yg9PBiDNt1EVqbW2kORVk= +k8s.io/apiextensions-apiserver v0.20.2/go.mod h1:F6TXp389Xntt+LUq3vw6HFOLttPa0V8821ogLGwb6Zs= k8s.io/apiextensions-apiserver v0.21.0/go.mod h1:gsQGNtGkc/YoDG9loKI0V+oLZM4ljRPjc/sql5tmvzc= k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= k8s.io/apiextensions-apiserver v0.21.2 h1:+exKMRep4pDrphEafRvpEi79wTnCFMqKf8LBtlA3yrE= k8s.io/apiextensions-apiserver v0.21.2/go.mod h1:+Axoz5/l3AYpGLlhJDfcVQzCerVYq3K3CvDMvw6X1RA= k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.21.0/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= k8s.io/apimachinery v0.21.2 h1:vezUc/BHqWlQDnZ+XkrpXSmnANSLbpnlpwo0Lhk0gpc= k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.2/go.mod h1:2nKd93WyMhZx4Hp3RfgH2K5PhwyTrprrkWYnI7id7jA= k8s.io/apiserver v0.21.0/go.mod h1:w2YSn4/WIwYuxG5zJmcqtRdtqgW/J2JRgFAqps3bBpg= k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= k8s.io/apiserver v0.21.2 h1:vfGLD8biFXHzbcIEXyW3652lDwkV8tZEFJAaS2iuJlw= @@ -1293,22 +1326,28 @@ k8s.io/cli-runtime v0.21.0/go.mod h1:XoaHP93mGPF37MkLbjGVYqg3S1MnsFdKtiA/RZzzxOo k8s.io/cli-runtime v0.21.1 h1:Oj/iZxa7LLXrhzShaLNF4rFJEIEBTDHj0dJw4ra2vX4= k8s.io/cli-runtime v0.21.1/go.mod h1:TI9Bvl8lQWZB2KqE91QLCp9AZE4l29zNFnj/x4IX4Fw= k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE= k8s.io/client-go v0.21.0/go.mod h1:nNBytTF9qPFDEhoqgEPaarobC8QPae13bElIVHzIglA= k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= k8s.io/client-go v0.21.2 h1:Q1j4L/iMN4pTw6Y4DWppBoUxgKO8LbffEMVEV00MUp0= k8s.io/client-go v0.21.2/go.mod h1:HdJ9iknWpbl3vMGtib6T2PyI/VYxiZfq936WNVHBRrA= k8s.io/component-base v0.18.0/go.mod h1:u3BCg0z1uskkzrnAKFzulmYaEpZF7XC9Pf/uFyb1v2c= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.2/go.mod h1:pzFtCiwe/ASD0iV7ySMu8SYVJjCapNM9bjvk7ptpKh0= k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= k8s.io/component-base v0.21.2 h1:EsnmFFoJ86cEywC0DoIkAUiEV6fjgauNugiw1lmIjs4= k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= k8s.io/component-helpers v0.21.0/go.mod h1:tezqefP7lxfvJyR+0a+6QtVrkZ/wIkyMLK4WcQ3Cj8U= k8s.io/component-helpers v0.21.1/go.mod h1:FtC1flbiQlosHQrLrRUulnKxE4ajgWCGy/67fT2GRlQ= +k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-aggregator v0.21.0 h1:my2WYu8RJcj/ZzWAjPPnmxNRELk/iCdPjMaOmsZOeBU= @@ -1322,6 +1361,8 @@ k8s.io/metrics v0.21.0/go.mod h1:L3Ji9EGPP1YBbfm9sPfEXSpnj8i24bfQbAFAsW0NueQ= k8s.io/metrics v0.21.1/go.mod h1:pyDVLsLe++FIGDBFU80NcW4xMFsuiVTWL8Zfi7+PpNo= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210111153108-fddb29f9d009/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210305010621-2afb4311ab10/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210527160623-6fdb442a123b h1:MSqsVQ3pZvPGTqCjptfimO2WjG7A9un2zcpiHkA6M/s= k8s.io/utils v0.0.0-20210527160623-6fdb442a123b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -1330,13 +1371,18 @@ rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19 h1:0jaDAAxtqIrrqas4vtTqxct4xS5kHfRNycTRLTyJmVM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/controller-runtime v0.8.3/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf5YkZNx2e0sU= sigs.k8s.io/controller-runtime v0.9.2 h1:MnCAsopQno6+hI9SgJHKddzXpmv2wtouZz6931Eax+Q= sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk= +sigs.k8s.io/controller-tools v0.5.0/go.mod h1:JTsstrMpxs+9BUj6eGuAaEb6SDSPTeVtUyp0jmnAM/I= sigs.k8s.io/controller-tools v0.6.0 h1:o2Fm1K7CmIp8OVaBtXsWB/ssBAzyoKZPPAGR3VuxaKs= sigs.k8s.io/controller-tools v0.6.0/go.mod h1:baRMVPrctU77F+rfAuH2uPqW93k6yQnZA2dhUOr7ihc= +sigs.k8s.io/gateway-api v0.3.0 h1:mKbQRlRIIY3dsCCbNF9Jv30V9vvOf6SRG82l0MfJQ9U= +sigs.k8s.io/gateway-api v0.3.0/go.mod h1:Wb8bx7QhGVZxOSEU3i9vw/JqTB5Nlai9MLMYVZeDmRQ= sigs.k8s.io/kustomize/api v0.8.5/go.mod h1:M377apnKT5ZHJS++6H4rQoCHmWtt6qTpp3mbe7p6OLY= sigs.k8s.io/kustomize/api v0.8.8 h1:G2z6JPSSjtWWgMeWSoHdXqyftJNmMmyxXpwENGoOtGE= sigs.k8s.io/kustomize/api v0.8.8/go.mod h1:He1zoK0nk43Pc6NlV085xDXDXTNprtcyKZVm3swsdNY= diff --git a/hack/build/repos.bzl b/hack/build/repos.bzl index 6bfc994c6..7ace62715 100644 --- a/hack/build/repos.bzl +++ b/hack/build/repos.bzl @@ -58,6 +58,15 @@ def go_repositories(): sum = "h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=", version = "v1.0.1", ) + go_repository( + name = "com_github_ahmetb_gen_crd_api_reference_docs", + build_file_generation = "on", + build_file_proto_mode = "disable", + importpath = "github.com/ahmetb/gen-crd-api-reference-docs", + sum = "h1:Gz+Wcvp8CATSZBy3a2yj2IdkK1/LaVfy4JJlzxdmETY=", + version = "v0.2.1-0.20201224172655-df869c1245d4", + ) + go_repository( name = "com_github_akamai_akamaiopen_edgegrid_golang", build_file_generation = "on", @@ -2860,8 +2869,8 @@ def go_repositories(): build_file_generation = "on", build_file_proto_mode = "disable", importpath = "github.com/russross/blackfriday/v2", - sum = "h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=", - version = "v2.0.1", + sum = "h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=", + version = "v2.1.0", ) go_repository( name = "com_github_ryanuber_columnize", @@ -3673,6 +3682,14 @@ def go_repositories(): sum = "h1:o2Fm1K7CmIp8OVaBtXsWB/ssBAzyoKZPPAGR3VuxaKs=", version = "v0.6.0", ) + go_repository( + name = "io_k8s_sigs_gateway_api", + build_file_generation = "on", + build_file_proto_mode = "disable", + importpath = "sigs.k8s.io/gateway-api", + sum = "h1:mKbQRlRIIY3dsCCbNF9Jv30V9vvOf6SRG82l0MfJQ9U=", + version = "v0.3.0", + ) go_repository( name = "io_k8s_sigs_kustomize_api", diff --git a/pkg/controller/BUILD.bazel b/pkg/controller/BUILD.bazel index e4c431c12..a5aa419fb 100644 --- a/pkg/controller/BUILD.bazel +++ b/pkg/controller/BUILD.bazel @@ -32,6 +32,8 @@ go_library( "@io_k8s_client_go//tools/cache:go_default_library", "@io_k8s_client_go//tools/record:go_default_library", "@io_k8s_client_go//util/workqueue:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/clientset/versioned:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/informers/externalversions:go_default_library", "@io_k8s_utils//clock:go_default_library", ], ) @@ -50,11 +52,11 @@ filegroup( "//pkg/controller/acmechallenges:all-srcs", "//pkg/controller/acmeorders:all-srcs", "//pkg/controller/cainjector:all-srcs", + "//pkg/controller/certificate-shim:all-srcs", "//pkg/controller/certificaterequests:all-srcs", "//pkg/controller/certificates:all-srcs", "//pkg/controller/certificatesigningrequests:all-srcs", "//pkg/controller/clusterissuers:all-srcs", - "//pkg/controller/ingress-shim:all-srcs", "//pkg/controller/issuers:all-srcs", "//pkg/controller/test:all-srcs", ], diff --git a/pkg/controller/ingress-shim/BUILD.bazel b/pkg/controller/certificate-shim/BUILD.bazel similarity index 75% rename from pkg/controller/ingress-shim/BUILD.bazel rename to pkg/controller/certificate-shim/BUILD.bazel index 429e1f99e..e075ad4c5 100644 --- a/pkg/controller/ingress-shim/BUILD.bazel +++ b/pkg/controller/certificate-shim/BUILD.bazel @@ -3,11 +3,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ - "controller.go", "helper.go", "sync.go", ], - importpath = "github.com/jetstack/cert-manager/pkg/controller/ingress-shim", + importpath = "github.com/jetstack/cert-manager/pkg/controller/certificate-shim", visibility = ["//visibility:public"], deps = [ "//pkg/api/util:go_default_library", @@ -24,20 +23,17 @@ go_library( "@io_k8s_apimachinery//pkg/api/errors:go_default_library", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", "@io_k8s_apimachinery//pkg/labels:go_default_library", - "@io_k8s_apimachinery//pkg/util/errors:go_default_library", - "@io_k8s_apimachinery//pkg/util/runtime:go_default_library", - "@io_k8s_client_go//kubernetes:go_default_library", - "@io_k8s_client_go//listers/networking/v1beta1:go_default_library", - "@io_k8s_client_go//tools/cache:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library", + "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library", "@io_k8s_client_go//tools/record:go_default_library", - "@io_k8s_client_go//util/workqueue:go_default_library", + "@io_k8s_sigs_gateway_api//apis/v1alpha1:go_default_library", ], ) go_test( name = "go_default_test", srcs = [ - "controller_test.go", "helper_test.go", "sync_test.go", ], @@ -46,17 +42,18 @@ go_test( "//pkg/apis/acme/v1:go_default_library", "//pkg/apis/certmanager/v1:go_default_library", "//pkg/apis/meta/v1:go_default_library", - "//pkg/client/clientset/versioned:go_default_library", + "//pkg/controller:go_default_library", "//pkg/controller/test:go_default_library", "//test/unit/gen:go_default_library", + "@com_github_go_logr_logr//:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", - "@com_github_stretchr_testify//require: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", "@io_k8s_apimachinery//pkg/types:go_default_library", - "@io_k8s_client_go//kubernetes:go_default_library", + "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library", "@io_k8s_client_go//testing:go_default_library", + "@io_k8s_sigs_gateway_api//apis/v1alpha1:go_default_library", ], ) @@ -69,7 +66,11 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//pkg/controller/certificate-shim/gateways:all-srcs", + "//pkg/controller/certificate-shim/ingresses:all-srcs", + ], tags = ["automanaged"], visibility = ["//visibility:public"], ) diff --git a/pkg/controller/certificate-shim/gateways/BUILD.bazel b/pkg/controller/certificate-shim/gateways/BUILD.bazel new file mode 100644 index 000000000..1b9ae0cb8 --- /dev/null +++ b/pkg/controller/certificate-shim/gateways/BUILD.bazel @@ -0,0 +1,51 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["controller.go"], + importpath = "github.com/jetstack/cert-manager/pkg/controller/certificate-shim/gateways", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/controller:go_default_library", + "//pkg/controller/certificate-shim:go_default_library", + "//pkg/logs:go_default_library", + "@io_k8s_apimachinery//pkg/api/errors:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/util/runtime:go_default_library", + "@io_k8s_client_go//tools/cache:go_default_library", + "@io_k8s_client_go//util/workqueue:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/listers/apis/v1alpha1:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["controller_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/client/clientset/versioned:go_default_library", + "//pkg/controller/test:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + "@io_k8s_sigs_gateway_api//apis/v1alpha1:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/clientset/versioned:go_default_library", + ], +) diff --git a/pkg/controller/certificate-shim/gateways/controller.go b/pkg/controller/certificate-shim/gateways/controller.go new file mode 100644 index 000000000..664fa6a7c --- /dev/null +++ b/pkg/controller/certificate-shim/gateways/controller.go @@ -0,0 +1,157 @@ +/* +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 ( + "context" + "fmt" + "time" + + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + gwlisters "sigs.k8s.io/gateway-api/pkg/client/listers/apis/v1alpha1" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + controllerpkg "github.com/jetstack/cert-manager/pkg/controller" + shimhelper "github.com/jetstack/cert-manager/pkg/controller/certificate-shim" + logf "github.com/jetstack/cert-manager/pkg/logs" +) + +const ( + ControllerName = "gateway-shim" + + // resyncPeriod is set to 10 hours across cert-manager. These 10 hours come + // from a discussion on the controller-runtime project that boils down to: + // never change this without an explicit reason. + // https://github.com/kubernetes-sigs/controller-runtime/pull/88#issuecomment-408500629 + resyncPeriod = 10 * time.Hour +) + +type controller struct { + gatewayLister gwlisters.GatewayLister + sync shimhelper.SyncFn + + // For testing purposes. + queue workqueue.RateLimitingInterface +} + +func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + c.gatewayLister = ctx.GWShared.Networking().V1alpha1().Gateways().Lister() + log := logf.FromContext(ctx.RootContext, ControllerName) + c.sync = shimhelper.SyncFnFor(ctx.Recorder, log, ctx.CMClient, ctx.SharedInformerFactory.Certmanager().V1().Certificates().Lister(), ctx.IngressShimOptions) + + // We don't need to requeue Gateways on "Deleted" events, since our Sync + // function does nothing when the Gateway lister returns "not found". But we + // still do it for consistency with the rest of the controllers. + ctx.GWShared.Networking().V1alpha1().Gateways().Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{ + Queue: c.queue, + }) + + // Even thought the Gateway controller already re-queues the Gateway after + // creating a child Certificate, we still re-queue the Gateway when we + // receive an "Add" event for the Certificate (the workqueue de-duplicates + // keys, so we should not worry). + // + // Regarding "Update" events on Certificates, we need to requeue the parent + // Gateway because we need to check if the Certificate is still up to date. + // + // Regarding "Deleted" events on Certificates, we requeue the parent Gateway + // to immediately recreate the Certificate when the Certificate is deleted. + ctx.SharedInformerFactory.Certmanager().V1().Certificates().Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ + WorkFunc: certificateHandler(c.queue), + }) + + mustSync := []cache.InformerSynced{ + ctx.GWShared.Networking().V1alpha1().Gateways().Informer().HasSynced, + ctx.SharedInformerFactory.Certmanager().V1().Certificates().Informer().HasSynced, + } + + return c.queue, mustSync, nil +} + +func (c *controller) ProcessItem(ctx context.Context, key string) error { + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + runtime.HandleError(fmt.Errorf("invalid resource key: %s", key)) + return nil + } + + crt, err := c.gatewayLister.Gateways(namespace).Get(name) + + if err != nil { + if k8sErrors.IsNotFound(err) { + runtime.HandleError(fmt.Errorf("Gateway '%s' in work queue no longer exists", key)) + return nil + } + + return err + } + + return c.sync(ctx, crt) +} + +// Whenever a Certificate gets updated, added or deleted, we want to reconcile +// its parent Gateway. This parent Gateway is called "controller object". For +// example, the following Certificate "cert-1" is controlled by the Gateway +// "gateway-1": +// +// kind: Certificate +// metadata: Note that the owner +// namespace: cert-1 reference does not +// ownerReferences: have a namespace, +// - controller: true since owner refs +// apiVersion: networking.x-k8s.io/v1alpha1 only work inside +// kind: Gateway the same namespace. +// name: gateway-1 +// blockOwnerDeletion: true +// uid: 7d3897c2-ce27-4144-883a-e1b5f89bd65a +func certificateHandler(queue workqueue.RateLimitingInterface) func(obj interface{}) { + return func(obj interface{}) { + crt, ok := obj.(*cmapi.Certificate) + if !ok { + runtime.HandleError(fmt.Errorf("not a Certificate object: %#v", obj)) + return + } + + ref := metav1.GetControllerOf(crt) + if ref == nil { + // No controller should care about orphans being deleted or + // updated. + return + } + + // We don't check the apiVersion e.g. "networking.x-k8s.io/v1alpha1" + // because there is no chance that another object called "Gateway" be + // the controller of a Certificate. + if ref.Kind != "Gateway" { + return + } + + queue.Add(crt.Namespace + "/" + ref.Name) + } +} + +func init() { + controllerpkg.Register(ControllerName, func(ctx *controllerpkg.Context) (controllerpkg.Interface, error) { + return controllerpkg.NewBuilder(ctx, ControllerName). + For(&controller{queue: workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), ControllerName)}). + Complete() + }) +} diff --git a/pkg/controller/certificate-shim/gateways/controller_test.go b/pkg/controller/certificate-shim/gateways/controller_test.go new file mode 100644 index 000000000..416624f36 --- /dev/null +++ b/pkg/controller/certificate-shim/gateways/controller_test.go @@ -0,0 +1,228 @@ +/* +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 ( + "context" + "testing" + "time" + + testpkg "github.com/jetstack/cert-manager/pkg/controller/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + gwapi "sigs.k8s.io/gateway-api/apis/v1alpha1" + gwclient "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var gatewayGVK = gwapi.SchemeGroupVersion.WithKind("Gateway") + +func Test_controller_Register(t *testing.T) { + tests := []struct { + name string + existingCert *cmapi.Certificate + givenCall func(*testing.T, cmclient.Interface, gwclient.Interface) + expectAddCalls []interface{} + }{ + { + name: "gateway is re-queued when an 'Added' event is received for this gateway", + givenCall: func(t *testing.T, _ cmclient.Interface, c gwclient.Interface) { + _, err := c.NetworkingV1alpha1().Gateways("namespace-1").Create(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-1", + }}, metav1.CreateOptions{}) + require.NoError(t, err) + }, + expectAddCalls: []interface{}{"namespace-1/gateway-1"}, + }, + { + name: "gateway is re-queued when an 'Updated' event is received for this gateway", + givenCall: func(t *testing.T, _ cmclient.Interface, c gwclient.Interface) { + // We can't use the gateway-api fake.NewSimpleClientset due to + // Gateway being pluralized as "gatewaies" instead of + // "gateways". The trick is thus to use Create instead. + _, err := c.NetworkingV1alpha1().Gateways("namespace-1").Create(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-1", + }}, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = c.NetworkingV1alpha1().Gateways("namespace-1").Update(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-1", Labels: map[string]string{"foo": "bar"}, + }}, metav1.UpdateOptions{}) + require.NoError(t, err) + }, + expectAddCalls: []interface{}{"namespace-1/gateway-1", "namespace-1/gateway-1"}, + // <----- Create ------> <------ Update -----> + }, + { + name: "gateway is re-queued when a 'Deleted' event is received for this gateway", + givenCall: func(t *testing.T, _ cmclient.Interface, c gwclient.Interface) { + _, err := c.NetworkingV1alpha1().Gateways("namespace-1").Create(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-1", + }}, metav1.CreateOptions{}) + require.NoError(t, err) + + err = c.NetworkingV1alpha1().Gateways("namespace-1").Delete(context.Background(), "gateway-1", metav1.DeleteOptions{}) + require.NoError(t, err) + }, + expectAddCalls: []interface{}{"namespace-1/gateway-1", "namespace-1/gateway-1"}, + // <----- Create ------> <------ Delete -----> + }, + { + name: "gateway is re-queued when an 'Added' event is received for its child Certificate", + givenCall: func(t *testing.T, c cmclient.Interface, _ gwclient.Interface) { + _, err := c.CertmanagerV1().Certificates("namespace-1").Create(context.Background(), &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "cert-1", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-2", + }}, gatewayGVK)}, + }}, metav1.CreateOptions{}) + require.NoError(t, err) + }, + expectAddCalls: []interface{}{"namespace-1/gateway-2"}, + }, + { + name: "gateway is re-queued when an 'Updated' event is received for its child Certificate", + existingCert: &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "cert-1", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-2", + }}, gatewayGVK)}, + }}, + givenCall: func(t *testing.T, c cmclient.Interface, _ gwclient.Interface) { + _, err := c.CertmanagerV1().Certificates("namespace-1").Update(context.Background(), &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "cert-1", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-2", + }}, gatewayGVK)}, + }}, metav1.UpdateOptions{}) + require.NoError(t, err) + }, + expectAddCalls: []interface{}{"namespace-1/gateway-2"}, + }, + { + name: "gateway is re-queued when a 'Deleted' event is received for its child Certificate", + existingCert: &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "cert-1", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace-1", Name: "gateway-2", + }}, gatewayGVK)}, + }}, + givenCall: func(t *testing.T, c cmclient.Interface, _ gwclient.Interface) { + // err := c.CertmanagerV1().Certificates("namespace-1").Delete(context.Background(), "cert-1", metav1.DeleteOptions{}) + // require.NoError(t, err) + }, + expectAddCalls: []interface{}{"namespace-1/gateway-2"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + var o []runtime.Object + if test.existingCert != nil { + o = append(o, test.existingCert) + } + + // NOTE(mael): we can't use Gateway with GWObjects because of a + // limitation in client-go's NewSimpleClientset. It uses a heuristic + // that wrongly guesses the resource from the Gateway kind + // ("gatewaies" instead of "gateways"). To work around this, the + // only way is to either use a real apiserver or to use call Create + // instead of setting existing objects with NewSimpleClientset. See: + // https://github.com/kubernetes/client-go/blob/7a90b0858/testing/fixture.go#L326-L331 + b := &testpkg.Builder{T: t, CertManagerObjects: o} + + b.Init() + + // We don't care about the HasSynced functions since we already know + // whether they have been properly "used": if no Gateway or + // Certificate event is received then HasSynced has not been setup + // properly. + mock := &mockWorkqueue{t: t} + _, _, err := (&controller{queue: mock}).Register(b.Context) + require.NoError(t, err) + + b.Start() + defer b.Stop() + + test.givenCall(t, b.CMClient, b.GWClient) + + // We have no way of knowing when the informers will be done adding + // items to the queue due to the "shared informer" architecture: + // Start(stop) does not allow you to wait for the informers to be + // done. + time.Sleep(50 * time.Millisecond) + + // We only expect 0 or 1 keys received in the queue, or 2 keys when + // we have to create a Gateway before deleting or updating it. + assert.Equal(t, test.expectAddCalls, mock.callsToAdd) + }) + } +} + +type mockWorkqueue struct { + t *testing.T + callsToAdd []interface{} +} + +func (m *mockWorkqueue) Add(arg0 interface{}) { + m.callsToAdd = append(m.callsToAdd, arg0) +} + +func (m *mockWorkqueue) AddAfter(arg0 interface{}, arg1 time.Duration) { + m.t.Error("workqueue.AddAfter was called but was not expected to be called") +} + +func (m *mockWorkqueue) AddRateLimited(arg0 interface{}) { + m.t.Error("workqueue.AddRateLimited was called but was not expected to be called") +} + +func (m *mockWorkqueue) Done(arg0 interface{}) { + m.t.Error("workqueue.Done was called but was not expected to be called") +} + +func (m *mockWorkqueue) Forget(arg0 interface{}) { + m.t.Error("workqueue.Forget was called but was not expected to be called") +} + +func (m *mockWorkqueue) Get() (interface{}, bool) { + m.t.Error("workqueue.Get was called but was not expected to be called") + return nil, false +} + +func (m *mockWorkqueue) Len() int { + m.t.Error("workqueue.Len was called but was not expected to be called") + return 0 +} + +func (m *mockWorkqueue) NumRequeues(arg0 interface{}) int { + m.t.Error("workqueue.NumRequeues was called but was not expected to be called") + return 0 +} + +func (m *mockWorkqueue) ShutDown() { + m.t.Error("workqueue.NumRequeues was called but was not expected to be called") +} + +func (m *mockWorkqueue) ShuttingDown() bool { + m.t.Error("workqueue.NumRequeues was called but was not expected to be called") + return false +} diff --git a/pkg/controller/ingress-shim/helper.go b/pkg/controller/certificate-shim/helper.go similarity index 95% rename from pkg/controller/ingress-shim/helper.go rename to pkg/controller/certificate-shim/helper.go index 1e0fea326..d44f0a740 100644 --- a/pkg/controller/ingress-shim/helper.go +++ b/pkg/controller/certificate-shim/helper.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package shimhelper import ( "errors" @@ -33,7 +33,7 @@ var ( errInvalidIngressAnnotation = errors.New("invalid ingress annotation") ) -func translateIngressAnnotations(crt *cmapi.Certificate, annotations map[string]string) error { +func translateAnnotations(crt *cmapi.Certificate, annotations map[string]string) error { if crt == nil { return errNilCertificate } diff --git a/pkg/controller/ingress-shim/helper_test.go b/pkg/controller/certificate-shim/helper_test.go similarity index 98% rename from pkg/controller/ingress-shim/helper_test.go rename to pkg/controller/certificate-shim/helper_test.go index b1b28fa73..dbeb20f2f 100644 --- a/pkg/controller/ingress-shim/helper_test.go +++ b/pkg/controller/certificate-shim/helper_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package shimhelper import ( "errors" @@ -110,7 +110,7 @@ func TestTranslateIngressAnnotations(t *testing.T) { } crt := tc.crt.DeepCopy() - err := translateIngressAnnotations(crt, tc.annotations) + err := translateAnnotations(crt, tc.annotations) if tc.expectedError != nil { assertErrorIs(t, err, tc.expectedError) diff --git a/pkg/controller/certificate-shim/ingresses/BUILD.bazel b/pkg/controller/certificate-shim/ingresses/BUILD.bazel new file mode 100644 index 000000000..cac49adf1 --- /dev/null +++ b/pkg/controller/certificate-shim/ingresses/BUILD.bazel @@ -0,0 +1,51 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["controller.go"], + importpath = "github.com/jetstack/cert-manager/pkg/controller/certificate-shim/ingresses", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/controller:go_default_library", + "//pkg/controller/certificate-shim:go_default_library", + "//pkg/logs:go_default_library", + "@io_k8s_apimachinery//pkg/api/errors:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/util/runtime:go_default_library", + "@io_k8s_client_go//listers/networking/v1beta1:go_default_library", + "@io_k8s_client_go//tools/cache:go_default_library", + "@io_k8s_client_go//util/workqueue:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["controller_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/client/clientset/versioned:go_default_library", + "//pkg/controller/test:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require: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", + "@io_k8s_client_go//kubernetes:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/controller/ingress-shim/controller.go b/pkg/controller/certificate-shim/ingresses/controller.go similarity index 78% rename from pkg/controller/ingress-shim/controller.go rename to pkg/controller/certificate-shim/ingresses/controller.go index 4fd35e5ea..d88c61d94 100644 --- a/pkg/controller/ingress-shim/controller.go +++ b/pkg/controller/certificate-shim/ingresses/controller.go @@ -20,20 +20,16 @@ import ( "context" "fmt" - "github.com/go-logr/logr" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes" networkinglisters "k8s.io/client-go/listers/networking/v1beta1" "k8s.io/client-go/tools/cache" - "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" - clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" - cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1" controllerpkg "github.com/jetstack/cert-manager/pkg/controller" + shimhelper "github.com/jetstack/cert-manager/pkg/controller/certificate-shim" logf "github.com/jetstack/cert-manager/pkg/logs" ) @@ -41,32 +37,20 @@ const ( ControllerName = "ingress-shim" ) -type defaults struct { - autoCertificateAnnotations []string - issuerName, issuerKind, issuerGroup string -} - type controller struct { - kClient kubernetes.Interface - cmClient clientset.Interface - - recorder record.EventRecorder - log logr.Logger - - ingressLister networkinglisters.IngressLister - certificateLister cmlisters.CertificateLister - - defaults defaults + ingressLister networkinglisters.IngressLister + sync shimhelper.SyncFn } -// Register registers and constructs the controller using the provided context. -// It returns the workqueue to be used to enqueue items, a list of -// InformerSynced functions that must be synced, or an error. func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { kShared := ctx.KubeSharedInformerFactory cmShared := ctx.SharedInformerFactory - c.log = logf.FromContext(ctx.RootContext, ControllerName) + c.ingressLister = kShared.Networking().V1beta1().Ingresses().Lister() + + log := logf.FromContext(ctx.RootContext, ControllerName) + c.sync = shimhelper.SyncFnFor(ctx.Recorder, log, ctx.CMClient, cmShared.Certmanager().V1().Certificates().Lister(), ctx.IngressShimOptions) + queue := workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), ControllerName) mustSync := []cache.InformerSynced{ @@ -74,9 +58,6 @@ func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitin cmShared.Certmanager().V1().Certificates().Informer().HasSynced, } - c.ingressLister = kShared.Networking().V1beta1().Ingresses().Lister() - c.certificateLister = cmShared.Certmanager().V1().Certificates().Lister() - // We still requeue on "Deleted" for consistency with the rest of the // controllers, but we don't actually need to. "Deleted" is only emitted // after the apiserver has removed the object entirely from etcd; if we had @@ -100,16 +81,6 @@ func (c *controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitin WorkFunc: certificateHandler(queue), }) - c.kClient = ctx.Client - c.cmClient = ctx.CMClient - c.recorder = ctx.Recorder - c.defaults = defaults{ - ctx.DefaultAutoCertificateAnnotations, - ctx.DefaultIssuerName, - ctx.DefaultIssuerKind, - ctx.DefaultIssuerGroup, - } - return queue, mustSync, nil } @@ -136,16 +107,17 @@ func (c *controller) ProcessItem(ctx context.Context, key string) error { // Whenever a Certificate gets updated, added or deleted, we want to reconcile // its parent Ingress. This parent Ingress is called "controller object". For -// example, the following Certificate is controlled by the Ingress "example": +// example, the following Certificate "cert-1" is controlled by the Ingress +// "ingress-1": // // kind: Certificate // metadata: Note that the owner -// namespace: cert-that-was-deleted reference does not +// namespace: cert-1 reference does not // ownerReferences: have a namespace, // - controller: true since owner refs // apiVersion: networking.k8s.io/v1beta1 only work inside // kind: Ingress the same namespace. -// name: example +// name: ingress-1 // blockOwnerDeletion: true // uid: 7d3897c2-ce27-4144-883a-e1b5f89bd65a func certificateHandler(queue workqueue.RateLimitingInterface) func(obj interface{}) { diff --git a/pkg/controller/ingress-shim/controller_test.go b/pkg/controller/certificate-shim/ingresses/controller_test.go similarity index 98% rename from pkg/controller/ingress-shim/controller_test.go rename to pkg/controller/certificate-shim/ingresses/controller_test.go index 2dc6272ec..bddaa29fc 100644 --- a/pkg/controller/ingress-shim/controller_test.go +++ b/pkg/controller/certificate-shim/ingresses/controller_test.go @@ -33,6 +33,8 @@ import ( cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" ) +var ingressGVK = networkingv1beta1.SchemeGroupVersion.WithKind("Ingress") + func Test_controller_Register(t *testing.T) { tests := []struct { name string @@ -129,7 +131,7 @@ func Test_controller_Register(t *testing.T) { b.Init() // We don't care about the HasSynced functions since we already know - // whether they have been properly "used": if no Ingress or + // whether they have been properly "used": if no Gateway or // Certificate event is received then HasSynced has not been setup // properly. queue, _, err := (&controller{}).Register(b.Context) diff --git a/pkg/controller/certificate-shim/sync.go b/pkg/controller/certificate-shim/sync.go new file mode 100644 index 000000000..1537fd08a --- /dev/null +++ b/pkg/controller/certificate-shim/sync.go @@ -0,0 +1,598 @@ +/* +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 shimhelper + +import ( + "context" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/record" + + "github.com/go-logr/logr" + cmacme "github.com/jetstack/cert-manager/pkg/apis/acme/v1" + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" + clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" + cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1" + "github.com/jetstack/cert-manager/pkg/controller" + logf "github.com/jetstack/cert-manager/pkg/logs" + "k8s.io/apimachinery/pkg/util/validation/field" + gwapi "sigs.k8s.io/gateway-api/apis/v1alpha1" +) + +const ( + reasonBadConfig = "BadConfig" + reasonCreateCertificate = "CreateCertificate" + reasonUpdateCertificate = "UpdateCertificate" + reasonDeleteCertificate = "DeleteCertificate" +) + +var ingressGVK = networkingv1beta1.SchemeGroupVersion.WithKind("Ingress") +var gatewayGVK = gwapi.SchemeGroupVersion.WithKind("Gateway") + +// SyncFn is the reconciliation function passed to a certificate-shim's +// controller. +type SyncFn func(context.Context, metav1.Object) error + +// SyncFnFor contains logic to reconcile any "Ingress-like" object. +// +// An "Ingress-like" object is a resource such as an Ingress, a Gateway or an +// HTTPRoute. Due to their similarity, the reconciliation function for them is +// common. Reconciling an Ingress-like object means looking at its annotations +// and creating a Certificate with matching DNS names and secretNames from the +// TLS configuration of the Ingress-like object. +func SyncFnFor( + rec record.EventRecorder, + log logr.Logger, + cmClient clientset.Interface, + cmLister cmlisters.CertificateLister, + defaults controller.IngressShimOptions, +) SyncFn { + return func(ctx context.Context, ingLike metav1.Object) error { + log := logf.WithResource(log, ingLike) + ctx = logf.NewContext(ctx, log) + + // rec.Eventf requires a runtime.Object, not a metav1.Object. + ingLikeObj, ok := ingLike.(runtime.Object) + if !ok { + return fmt.Errorf("programmer mistake: %T was expected to be a runtime.Object", ingLike) + } + + // The flag --auto-certificate-annotations as well as the default + // "kubernetes.io/tls-acme" annotation are only enabled for the Ingress + // resource. + var autoAnnotations []string + switch ingLike.(type) { + case *networkingv1beta1.Ingress: + autoAnnotations = defaults.DefaultAutoCertificateAnnotations + } + + if !hasShimAnnotation(ingLike, autoAnnotations) { + logf.V(logf.DebugLevel).Infof("not syncing ingress resource as it does not contain a %q or %q annotation", + cmapi.IngressIssuerNameAnnotationKey, cmapi.IngressClusterIssuerNameAnnotationKey) + return nil + } + + issuerName, issuerKind, issuerGroup, err := issuerForIngressLike(defaults, ingLike) + if err != nil { + log.Error(err, "failed to determine issuer to be used for ingress resource") + rec.Eventf(ingLikeObj, corev1.EventTypeWarning, reasonBadConfig, "Could not determine issuer for ingress due to bad annotations: %s", + err) + return nil + } + + err = validateIngressLike(ingLike).ToAggregate() + if err != nil { + rec.Eventf(ingLikeObj, corev1.EventTypeWarning, reasonBadConfig, err.Error()) + return nil + } + + newCrts, updateCrts, err := buildCertificates(rec, log, cmLister, ingLike, issuerName, issuerKind, issuerGroup) + if err != nil { + return err + } + + for _, crt := range newCrts { + _, err := cmClient.CertmanagerV1().Certificates(crt.Namespace).Create(ctx, crt, metav1.CreateOptions{}) + if err != nil { + return err + } + rec.Eventf(ingLikeObj, corev1.EventTypeNormal, reasonCreateCertificate, "Successfully created Certificate %q", crt.Name) + } + + for _, crt := range updateCrts { + _, err := cmClient.CertmanagerV1().Certificates(crt.Namespace).Update(ctx, crt, metav1.UpdateOptions{}) + if err != nil { + return err + } + rec.Eventf(ingLikeObj, corev1.EventTypeNormal, reasonUpdateCertificate, "Successfully updated Certificate %q", crt.Name) + } + + unrequiredCrts, err := findUnrequiredCertificates(cmLister, ingLike) + if err != nil { + return err + } + + for _, crt := range unrequiredCrts { + err = cmClient.CertmanagerV1().Certificates(crt.Namespace).Delete(ctx, crt.Name, metav1.DeleteOptions{}) + if err != nil { + return err + } + rec.Eventf(ingLikeObj, corev1.EventTypeNormal, reasonDeleteCertificate, "Successfully deleted unrequired Certificate %q", crt.Name) + } + + return nil + } +} + +func validateIngressLike(ingLike metav1.Object) field.ErrorList { + switch o := ingLike.(type) { + case *networkingv1beta1.Ingress: + return checkForDuplicateSecretNames(field.NewPath("spec", "tls"), o.Spec.TLS) + case *gwapi.Gateway: + return nil + default: + panic(fmt.Errorf("programmer mistake: validateIngressLike can't handle %T, expected Ingress or Gateway", ingLike)) + } +} + +func checkForDuplicateSecretNames(path *field.Path, tlsBlocks []networkingv1beta1.IngressTLS) field.ErrorList { + var errs field.ErrorList + // We can't let two TLS blocks share the same secretName because we decided + // to create one Certificate for each TLS block. For example: + // + // kind: Ingress + // spec: + // tls: + // - hosts: [example.com] + // secretName: example-tls + // - hosts: [www.example.com] + // secretName: example-tls + // + // With this Ingress, cert-manager would create two Certificates with the + // same name, which would fail. + // + // We keep track of the order of the secret names due to Go iterating on + // maps in a non-deterministic way. We also keep track of each secretName's + // JSON path just so that we can give a nice error message. + var secretNames []string + secretPaths := make(map[string][]*field.Path) + for i, tls := range tlsBlocks { + if _, already := secretPaths[tls.SecretName]; !already { + secretNames = append(secretNames, tls.SecretName) + } + secretPaths[tls.SecretName] = append(secretPaths[tls.SecretName], path.Index(i).Child("secretName")) + } + + for _, name := range secretNames { + paths := secretPaths[name] + if len(paths) > 1 { + // We could use field.Duplicate, but that would prevent us from + // giving details as to what this duplicate is about. + errs = append(errs, field.Invalid(paths[0], name, + fmt.Sprintf("this secret name must only appear in a single TLS entry but is also used in %s", paths[1]))) + } + } + + return errs +} + +func validateIngressTLSBlock(path *field.Path, tlsBlock networkingv1beta1.IngressTLS) field.ErrorList { + var errs field.ErrorList + + if len(tlsBlock.Hosts) == 0 { + errs = append(errs, field.Required(path.Child("hosts"), "")) + } + if tlsBlock.SecretName == "" { + errs = append(errs, field.Required(path.Child("secretName"), "")) + } + + return errs +} + +func validateGatewayListenerBlock(path *field.Path, l gwapi.Listener) field.ErrorList { + var errs field.ErrorList + + if l.Hostname == nil || *l.Hostname == "" { + errs = append(errs, field.Required(path.Child("hostname"), "the hostname cannot be empty")) + } + + if l.TLS == nil { + errs = append(errs, field.Required(path.Child("tls"), "the TLS block cannot be empty")) + return errs + } + + if l.TLS.CertificateRef == nil { + errs = append(errs, field.Required(path.Child("tls").Child("certificateRef"), + "listener is missing a certificateRef")) + } else { + if l.TLS.CertificateRef.Group != "core" { + errs = append(errs, field.NotSupported(path.Child("tls").Child("certificateRef").Child("group"), + l.TLS.CertificateRef.Group, []string{"core"})) + } + + if l.TLS.CertificateRef.Kind != "Secret" { + errs = append(errs, field.NotSupported(path.Child("tls").Child("certificateRef").Child("kind"), + l.TLS.CertificateRef.Kind, []string{"Secret"})) + } + + if l.TLS.CertificateRef.Name == "" { + errs = append(errs, field.Required(path.Child("tls").Child("certificateRef").Child("name"), + "the Secret name cannot be empty")) + } + } + + if l.TLS.Mode == nil { + errs = append(errs, field.Required(path.Child("tls").Child("mode"), + "the mode field is required")) + } else { + if *l.TLS.Mode != gwapi.TLSModeTerminate { + errs = append(errs, field.NotSupported(path.Child("tls").Child("mode"), + *l.TLS.Mode, []string{string(gwapi.TLSModeTerminate)})) + } + } + + return errs +} + +func buildCertificates( + rec record.EventRecorder, + log logr.Logger, + cmLister cmlisters.CertificateLister, + ingLike metav1.Object, + issuerName, issuerKind, issuerGroup string, +) (new, update []*cmapi.Certificate, _ error) { + + var newCrts []*cmapi.Certificate + var updateCrts []*cmapi.Certificate + + type certificateShimInfo struct { + // tlsHosts key = secret ref, value = dns host names + + gvk schema.GroupVersionKind + } + + tlsHosts := make(map[corev1.ObjectReference][]string) + switch ingLike := ingLike.(type) { + case *networkingv1beta1.Ingress: + for i, tls := range ingLike.Spec.TLS { + path := field.NewPath("spec", "tls").Index(i) + err := validateIngressTLSBlock(path, tls).ToAggregate() + if err != nil { + rec.Eventf(ingLike, corev1.EventTypeWarning, reasonBadConfig, "Skipped a TLS block: "+err.Error()) + continue + } + tlsHosts[corev1.ObjectReference{ + Namespace: ingLike.Namespace, + Name: tls.SecretName, + }] = tls.Hosts + } + case *gwapi.Gateway: + for i, l := range ingLike.Spec.Listeners { + err := validateGatewayListenerBlock(field.NewPath("spec", "listeners").Index(i), l).ToAggregate() + if err != nil { + rec.Eventf(ingLike, corev1.EventTypeWarning, reasonBadConfig, "Skipped a listener block: "+err.Error()) + continue + } + + secretRef := corev1.ObjectReference{ + Namespace: ingLike.Namespace, + Name: l.TLS.CertificateRef.Name, + } + // Gateway API hostname explicitly disallows IP addresses, so this + // should be OK. + tlsHosts[secretRef] = append(tlsHosts[secretRef], fmt.Sprintf("%s", *l.Hostname)) + } + default: + return nil, nil, fmt.Errorf("buildCertificates: expected ingress or gateway, got %T", ingLike) + } + + for secretRef, hosts := range tlsHosts { + existingCrt, err := cmLister.Certificates(secretRef.Namespace).Get(secretRef.Name) + if !apierrors.IsNotFound(err) && err != nil { + return nil, nil, err + } + + var controllerGVK schema.GroupVersionKind + switch ingLike.(type) { + case *networkingv1beta1.Ingress: + controllerGVK = ingressGVK + case *gwapi.Gateway: + controllerGVK = gatewayGVK + } + + crt := &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretRef.Name, + Namespace: secretRef.Namespace, + Labels: ingLike.GetLabels(), + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ingLike, controllerGVK)}, + }, + Spec: cmapi.CertificateSpec{ + DNSNames: hosts, + SecretName: secretRef.Name, + IssuerRef: cmmeta.ObjectReference{ + Name: issuerName, + Kind: issuerKind, + Group: issuerGroup, + }, + Usages: cmapi.DefaultKeyUsages(), + }, + } + + switch o := ingLike.(type) { + case *networkingv1beta1.Ingress: + ingLike = o.DeepCopy() + case *gwapi.Gateway: + ingLike = o.DeepCopy() + } + setIssuerSpecificConfig(crt, ingLike) + + if err := translateAnnotations(crt, ingLike.GetAnnotations()); err != nil { + return nil, nil, err + } + + // check if a Certificate for this TLS entry already exists, and if it + // does then skip this entry + if existingCrt != nil { + log := logf.WithRelatedResource(log, existingCrt) + log.V(logf.DebugLevel).Info("certificate already exists for this object, ensuring it is up to date") + + if metav1.GetControllerOf(existingCrt) == nil { + log.V(logf.InfoLevel).Info("certificate resource has no owner. refusing to update non-owned certificate resource for object") + continue + } + + if !metav1.IsControlledBy(existingCrt, ingLike) { + log.V(logf.InfoLevel).Info("certificate resource is not owned by this object. refusing to update non-owned certificate resource for object") + continue + } + + if !certNeedsUpdate(existingCrt, crt) { + log.V(logf.DebugLevel).Info("certificate resource is already up to date for object") + continue + } + + updateCrt := existingCrt.DeepCopy() + + updateCrt.Spec = crt.Spec + updateCrt.Labels = crt.Labels + + setIssuerSpecificConfig(crt, ingLike) + + updateCrts = append(updateCrts, updateCrt) + } else { + + newCrts = append(newCrts, crt) + } + } + return newCrts, updateCrts, nil +} + +func findUnrequiredCertificates(list cmlisters.CertificateLister, ingLike metav1.Object) ([]*cmapi.Certificate, error) { + crts, err := list.Certificates(ingLike.GetNamespace()).List(labels.Everything()) + if err != nil { + return nil, err + } + + var unrequired []*cmapi.Certificate + for _, crt := range crts { + if isUnrequiredCertificate(crt, ingLike) { + unrequired = append(unrequired, crt) + } + } + + return unrequired, nil +} + +func isUnrequiredCertificate(crt *cmapi.Certificate, ingLike metav1.Object) bool { + if !metav1.IsControlledBy(crt, ingLike) { + return false + } + + switch o := ingLike.(type) { + case *networkingv1beta1.Ingress: + for _, tls := range o.Spec.TLS { + if crt.Spec.SecretName == tls.SecretName { + return false + } + } + case *gwapi.Gateway: + for _, l := range o.Spec.Listeners { + if crt.Spec.SecretName == l.TLS.CertificateRef.Name { + return false + } + } + } + return true +} + +// certNeedsUpdate checks and returns true if two Certificates differ. +func certNeedsUpdate(a, b *cmapi.Certificate) bool { + if a.Name != b.Name { + return true + } + + // TODO: we may need to allow users to edit the managed Certificate resources + // to add their own labels directly. + // Right now, we'll reset/remove the label values back automatically. + // Let's hope no other controllers do this automatically, else we'll start fighting... + if !reflect.DeepEqual(a.Labels, b.Labels) { + return true + } + + if a.Spec.CommonName != b.Spec.CommonName { + return true + } + + if len(a.Spec.DNSNames) != len(b.Spec.DNSNames) { + return true + } + + for i := range a.Spec.DNSNames { + if a.Spec.DNSNames[i] != b.Spec.DNSNames[i] { + return true + } + } + + if a.Spec.SecretName != b.Spec.SecretName { + return true + } + + if a.Spec.IssuerRef.Name != b.Spec.IssuerRef.Name { + return true + } + + if a.Spec.IssuerRef.Kind != b.Spec.IssuerRef.Kind { + return true + } + + return false +} + +func setIssuerSpecificConfig(crt *cmapi.Certificate, ingLike metav1.Object) { + ingAnnotations := ingLike.GetAnnotations() + if ingAnnotations == nil { + ingAnnotations = map[string]string{} + } + + // for ACME issuers + editInPlaceVal := ingAnnotations[cmacme.IngressEditInPlaceAnnotationKey] + editInPlace := editInPlaceVal == "true" + if editInPlace { + if crt.Annotations == nil { + crt.Annotations = make(map[string]string) + } + crt.Annotations[cmacme.ACMECertificateHTTP01IngressNameOverride] = ingLike.GetName() + // set IssueTemporaryCertificateAnnotation to true in order to behave + // better when ingress-gce is being used. + crt.Annotations[cmapi.IssueTemporaryCertificateAnnotation] = "true" + } + + ingressClassVal, hasIngressClassVal := ingAnnotations[cmapi.IngressACMEIssuerHTTP01IngressClassAnnotationKey] + if hasIngressClassVal { + if crt.Annotations == nil { + crt.Annotations = make(map[string]string) + } + crt.Annotations[cmacme.ACMECertificateHTTP01IngressClassOverride] = ingressClassVal + } + + ingLike.SetAnnotations(ingAnnotations) +} + +// hasShimAnnotation returns true if this ingress-like object contains one of +// the annotations "cert-manager.io/issuer", "cert-manager.io/cluster-issuer", +// or one of the annotations provided with --auto-certificate-annotations (which +// default to "kubernetes.io/tls-acme"). +func hasShimAnnotation(ingLike metav1.Object, autoCertificateAnnotations []string) bool { + annotations := ingLike.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + if _, ok := annotations[cmapi.IngressIssuerNameAnnotationKey]; ok { + return true + } + if _, ok := annotations[cmapi.IngressClusterIssuerNameAnnotationKey]; ok { + return true + } + for _, x := range autoCertificateAnnotations { + if s, ok := annotations[x]; ok { + if b, _ := strconv.ParseBool(s); b { + return true + } + } + } + return false +} + +// issuerForIngressLike determines the Issuer that should be specified on a +// Certificate created for the given ingress-like resource. If one is not set, +// the default issuer given to the controller is used. +func issuerForIngressLike(defaults controller.IngressShimOptions, ingLike metav1.Object) (name, kind, group string, err error) { + var errs []string + + name = defaults.DefaultIssuerName + kind = defaults.DefaultIssuerKind + group = defaults.DefaultIssuerGroup + + annotations := ingLike.GetAnnotations() + + if annotations == nil { + annotations = map[string]string{} + } + + issuerName, issuerNameOK := annotations[cmapi.IngressIssuerNameAnnotationKey] + if issuerNameOK { + name = issuerName + kind = cmapi.IssuerKind + } + + clusterIssuerName, clusterIssuerNameOK := annotations[cmapi.IngressClusterIssuerNameAnnotationKey] + if clusterIssuerNameOK { + name = clusterIssuerName + kind = cmapi.ClusterIssuerKind + } + + kindName, kindNameOK := annotations[cmapi.IssuerKindAnnotationKey] + if kindNameOK { + kind = kindName + } + + groupName, groupNameOK := annotations[cmapi.IssuerGroupAnnotationKey] + if groupNameOK { + group = groupName + } + + if len(name) == 0 { + errs = append(errs, "failed to determine issuer name to be used for ingress resource") + } + + if issuerNameOK && clusterIssuerNameOK { + errs = append(errs, + fmt.Sprintf("both %q and %q may not be set", + cmapi.IngressIssuerNameAnnotationKey, cmapi.IngressClusterIssuerNameAnnotationKey)) + } + + if clusterIssuerNameOK && groupNameOK { + errs = append(errs, + fmt.Sprintf("both %q and %q may not be set", + cmapi.IngressClusterIssuerNameAnnotationKey, cmapi.IssuerGroupAnnotationKey)) + } + + if clusterIssuerNameOK && kindNameOK { + errs = append(errs, + fmt.Sprintf("both %q and %q may not be set", + cmapi.IngressClusterIssuerNameAnnotationKey, cmapi.IssuerKindAnnotationKey)) + } + + if len(errs) > 0 { + return "", "", "", errors.New(strings.Join(errs, ", ")) + } + + return name, kind, group, nil +} diff --git a/pkg/controller/certificate-shim/sync_test.go b/pkg/controller/certificate-shim/sync_test.go new file mode 100644 index 000000000..280668123 --- /dev/null +++ b/pkg/controller/certificate-shim/sync_test.go @@ -0,0 +1,2637 @@ +/* +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 shimhelper + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + networkingv1beta1 "k8s.io/api/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + coretesting "k8s.io/client-go/testing" + gwapi "sigs.k8s.io/gateway-api/apis/v1alpha1" + + cmacme "github.com/jetstack/cert-manager/pkg/apis/acme/v1" + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" + "github.com/jetstack/cert-manager/pkg/controller" + controllerpkg "github.com/jetstack/cert-manager/pkg/controller" + testpkg "github.com/jetstack/cert-manager/pkg/controller/test" + "github.com/jetstack/cert-manager/test/unit/gen" +) + +func Test_hasShimAnnotation(t *testing.T) { + type testT struct { + Annot map[string]string + Want bool + } + + t.Run("ingress", func(t *testing.T) { + tests := []testT{ + {Annot: map[string]string{"cert-manager.io/issuer": ""}, Want: true}, + {Annot: map[string]string{"cert-manager.io/cluster-issuer": ""}, Want: true}, + {Annot: map[string]string{"kubernetes.io/tls-acme": "true"}, Want: true}, + {Annot: map[string]string{"kubernetes.io/tls-acme": "false"}, Want: false}, + {Annot: map[string]string{"kubernetes.io/tls-acme": ""}, Want: false}, + {Annot: nil, Want: false}, + } + for _, test := range tests { + shouldSyncIngress := hasShimAnnotation(buildIngress("", "", test.Annot), []string{"kubernetes.io/tls-acme"}) + if shouldSyncIngress != test.Want { + t.Errorf("Expected shouldSyncIngress=%v for annotations %#v", test.Want, test.Annot) + } + shouldSyncGateway := hasShimAnnotation(buildGateway("", "", test.Annot), []string{"kubernetes.io/tls-acme"}) + if shouldSyncGateway != test.Want { + t.Errorf("Expected shouldSyncGateway=%v for annotations %#v", test.Want, test.Annot) + } + } + }) +} + +func TestSync(t *testing.T) { + clusterIssuer := gen.ClusterIssuer("issuer-name") + acmeIssuerNewFormat := gen.Issuer("issuer-name", + gen.SetIssuerACME(cmacme.ACMEIssuer{})) + acmeIssuer := gen.Issuer("issuer-name", + gen.SetIssuerACME(cmacme.ACMEIssuer{})) + acmeClusterIssuer := gen.ClusterIssuer("issuer-name", + gen.SetIssuerACME(cmacme.ACMEIssuer{})) + type testT struct { + Name string + IngressLike metav1.Object + Issuer cmapi.GenericIssuer + IssuerLister []runtime.Object + ClusterIssuerLister []runtime.Object + CertificateLister []runtime.Object + DefaultIssuerName string + DefaultIssuerKind string + DefaultIssuerGroup string + Err bool + ExpectedCreate []*cmapi.Certificate + ExpectedUpdate []*cmapi.Certificate + ExpectedDelete []*cmapi.Certificate + ExpectedEvents []string + } + testIngressShim := []testT{ + { + Name: "return a single Certificate for an ingress with a single valid TLS entry and common-name annotation", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.CommonNameAnnotationKey: "my-cn", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com", "www.example.com"}, + CommonName: "my-cn", + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations using edit-in-place", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmacme.IngressEditInPlaceAnnotationKey: "true", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmacme.ACMECertificateHTTP01IngressNameOverride: "ingress-name", + cmapi.IssueTemporaryCertificateAnnotation: "true", + }, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "create a Certificate with the HTTP01 name override if the given ingress uses http01 annotations", + Issuer: gen.Issuer(acmeIssuer.Name), + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmacme.IngressEditInPlaceAnnotationKey: "true", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmacme.ACMECertificateHTTP01IngressNameOverride: "ingress-name", + cmapi.IssueTemporaryCertificateAnnotation: "true", + }, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations with no ingress class set", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations with a custom ingress class", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "nginx-ing", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations with a certificate ingress class", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressACMEIssuerHTTP01IngressClassAnnotationKey: "cert-ing", + cmapi.IngressClassAnnotationKey: "nginx-ing", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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), + Annotations: map[string]string{ + cmacme.ACMECertificateHTTP01IngressClassOverride: "cert-ing", + }, + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "edit-in-place set to false should not trigger editing the ingress in-place", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "nginx-ing", + cmacme.IngressEditInPlaceAnnotationKey: "false", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single DNS01 Certificate for an ingress with a single valid TLS entry", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should return a basic certificate when no provider specific config is provided", + Issuer: clusterIssuer, + DefaultIssuerName: "issuer-name", + DefaultIssuerKind: "ClusterIssuer", + DefaultIssuerGroup: "cert-manager.io", + ClusterIssuerLister: []runtime.Object{clusterIssuer}, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + "kubernetes.io/tls-acme": "true", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com", "www.example.com"}, + SecretName: "example-com-tls", + }, + }, + }, + }, + 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", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should skip an invalid TLS entry (no TLS hosts specified)", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + ExpectedEvents: []string{ + `Warning BadConfig Skipped a TLS block: spec.tls[0].hosts: Required value`, + `Normal CreateCertificate Successfully created Certificate "example-com-tls"`, + }, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + SecretName: "example-com-tls-invalid", + }, + { + SecretName: "example-com-tls", + Hosts: []string{"example.com", "www.example.com"}, + }, + }, + }, + }, + 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", + Usages: cmapi.DefaultKeyUsages(), + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + }, + }, + }, + }, + + { + Name: "should skip an invalid TLS entry (no TLS secret name specified)", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + ExpectedEvents: []string{ + `Warning BadConfig Skipped a TLS block: spec.tls[0].secretName: Required value`, + `Normal CreateCertificate Successfully created Certificate "example-com-tls"`, + }, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + }, + { + Hosts: []string{"example.com", "www.example.com"}, + SecretName: "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", + Usages: cmapi.DefaultKeyUsages(), + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + }, + }, + }, + }, + { + Name: "should error if the specified issuer is not found", + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "invalid-issuer-name", + }, + UID: types.UID("ingress-name"), + }, + }, + }, + { + Name: "should not return any certificates if a correct Certificate already exists", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + SecretName: "existing-crt", + }, + }, + }, + }, + DefaultIssuerKind: "Issuer", + DefaultIssuerGroup: "cert-manager.io", + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should update a certificate if an incorrect Certificate exists", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + SecretName: "existing-crt", + }, + }, + }, + }, + CertificateLister: []runtime.Object{ + buildCertificate("existing-crt", + gen.DefaultTestNamespace, + buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + ), + }, + DefaultIssuerKind: "Issuer", + ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "existing-crt"`}, + ExpectedUpdate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should update an existing Certificate resource with new labels if they do not match those specified on the IngressLike", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuerNewFormat}, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + SecretName: "cert-secret-name", + }, + }, + }, + }, + DefaultIssuerKind: "Issuer", + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert-secret-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "a-different-value": "should be removed", + }, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "cert-secret-name", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "cert-secret-name"`}, + ExpectedUpdate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cert-secret-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "cert-secret-name", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should not update certificate if it does not belong to any ingress", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "toot-ing", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + SecretName: "existing-crt", + }, + }, + }, + }, + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: []metav1.OwnerReference{}, + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should not update certificate if it does not belong to the ingress", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "toot-ing", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + SecretName: "existing-crt", + }, + }, + }, + }, + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildIngressOwnerReferences("not-ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should delete a Certificate if its SecretName is not present in the ingress", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("ingress-name"), + }, + }, + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + ExpectedEvents: []string{`Normal DeleteCertificate Successfully deleted unrequired Certificate "existing-crt"`}, + ExpectedDelete: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + }, + }, + }, + }, + { + Name: "should update a Certificate if is contains a Common Name that is not defined on the ingress annotations", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &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", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + SecretName: "example-com-tls", + }, + }, + }, + }, + CertificateLister: []runtime.Object{ + &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"}, + SecretName: "example-com-tls", + CommonName: "example-common-name", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "example-com-tls"`}, + ExpectedUpdate: []*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"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "if an ingress contains multiple tls entries that specify the same secretName, an error should be logged and no action taken", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + ExpectedEvents: []string{ + `Warning BadConfig spec.tls[0].secretName: Invalid value: "example-com-tls": this secret name must only appear in a single TLS entry but is also used in spec.tls[1].secretName`, + }, + IngressLike: &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", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.IngressTLS{ + { + Hosts: []string{"example.com"}, + SecretName: "example-com-tls", + }, + { + Hosts: []string{"notexample.com"}, + SecretName: "example-com-tls", + }, + }, + }, + }, + }, + { + Name: "Failure to translateIngressAnnotations", + Issuer: acmeIssuer, + IngressLike: &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, + }, + { + Name: "return a single Certificate for an ingress with a single valid TLS entry with common-name and keyusage annotation", + Issuer: acmeClusterIssuer, + IngressLike: &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.CommonNameAnnotationKey: "my-cn", + "cert-manager.io/usages": "signing,digital signature,content commitment", + }, + UID: types.UID("ingress-name"), + }, + Spec: networkingv1beta1.IngressSpec{ + TLS: []networkingv1beta1.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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + OwnerReferences: buildIngressOwnerReferences("ingress-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com", "www.example.com"}, + CommonName: "my-cn", + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: []cmapi.KeyUsage{ + cmapi.UsageSigning, + cmapi.UsageDigitalSignature, + cmapi.UsageContentCommitment, + }, + }, + }, + }, + }, + } + + testGatewayShim := []testT{ + { + Name: "return a single Certificate for a Gateway with a single valid TLS entry and common-name annotation", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.CommonNameAnnotationKey: "my-cn", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{ + { + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + CommonName: "my-cn", + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for a Gateway with a single valid TLS entry and HTTP01 annotations using edit-in-place", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmacme.IngressEditInPlaceAnnotationKey: "true", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmacme.ACMECertificateHTTP01IngressNameOverride: "gateway-name", + cmapi.IssueTemporaryCertificateAnnotation: "true", + }, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "create a Certificate with the HTTP01 name override if the given Gateway uses http01 annotations", + Issuer: gen.Issuer(acmeIssuer.Name), + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmacme.IngressEditInPlaceAnnotationKey: "true", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmacme.ACMECertificateHTTP01IngressNameOverride: "gateway-name", + cmapi.IssueTemporaryCertificateAnnotation: "true", + }, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for an Gateway with a single valid TLS entry and HTTP01 annotations with no gateway class set", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for an Gateway with a single valid TLS entry and HTTP01 annotations with a custom gateway class", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "nginx-ing", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single HTTP01 Certificate for an Gateway with a single valid TLS entry and HTTP01 annotations with a certificate Gateway class", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressACMEIssuerHTTP01IngressClassAnnotationKey: "cert-ing", + cmapi.IngressClassAnnotationKey: "nginx-ing", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + Annotations: map[string]string{ + cmacme.ACMECertificateHTTP01IngressClassOverride: "cert-ing", + }, + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "edit-in-place set to false should not trigger editing the Gateway in-place", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "nginx-ing", + cmacme.IngressEditInPlaceAnnotationKey: "false", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "return a single DNS01 Certificate for a Gateway with a single valid TLS entry", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "kubernetes.io/tls-acme should not trigger a Gateway", + Issuer: clusterIssuer, + DefaultIssuerName: "issuer-name", + DefaultIssuerKind: "ClusterIssuer", + DefaultIssuerGroup: "cert-manager.io", + ClusterIssuerLister: []runtime.Object{clusterIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + "kubernetes.io/tls-acme": "true", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }}, + }, + }, + }, + { + Name: "should skip an invalid TLS entry (no TLS hosts specified)", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + ExpectedEvents: []string{ + `Warning BadConfig Skipped a listener block: spec.listeners[1].hostname: Required value: the hostname cannot be empty`, + `Normal CreateCertificate Successfully created Certificate "example-com-tls"`, + }, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }, { + Hostname: nil, // 🔥 + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls-invalid", + }, + }, + }}, + }, + }, + ExpectedCreate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com-tls", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + Usages: cmapi.DefaultKeyUsages(), + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + }, + }, + }, + }, + { + Name: "should skip an invalid TLS entry (no TLS secret name specified)", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + ExpectedEvents: []string{ + `Warning BadConfig Skipped a listener block: spec.listeners[0].tls.certificateRef: Required value: listener is missing a certificateRef`, + `Normal CreateCertificate Successfully created Certificate "example-com-tls"`, + }, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: nil, // 🔥 + }, + }, { + Hostname: ptrHostname("www.example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }}, + }, + }, + ExpectedCreate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com-tls", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"www.example.com"}, + SecretName: "example-com-tls", + Usages: cmapi.DefaultKeyUsages(), + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + }, + }, + }, + }, + { + Name: "should error if the specified issuer is not found", + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "invalid-issuer-name", + }, + UID: types.UID("gateway-name"), + }, + }, + }, + { + Name: "should not return any certificates if a correct Certificate already exists", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "existing-crt", + }, + }, + }}, + }, + }, + DefaultIssuerKind: "Issuer", + DefaultIssuerGroup: "cert-manager.io", + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should update a certificate if an incorrect Certificate exists", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "existing-crt", + }, + }, + }}, + }, + }, + CertificateLister: []runtime.Object{ + buildCertificate("existing-crt", + gen.DefaultTestNamespace, + buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + ), + }, + DefaultIssuerKind: "Issuer", + ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "existing-crt"`}, + ExpectedUpdate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should update an existing Certificate resource with new labels if they do not match those specified on the Gateway", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuerNewFormat}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "cert-secret-name", + }, + }, + }}, + }, + }, + DefaultIssuerKind: "Issuer", + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert-secret-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "a-different-value": "should be removed", + }, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "cert-secret-name", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "cert-secret-name"`}, + ExpectedUpdate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cert-secret-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "cert-secret-name", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should not update certificate if it does not belong to any Gateway", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "toot-ing", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "existing-crt", + }, + }, + }}, + }, + }, + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: []metav1.OwnerReference{}, + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should not update certificate if it does not belong to the Gateway", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + cmapi.IngressClassAnnotationKey: "toot-ing", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "existing-crt", + }, + }, + }}, + }, + }, + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildIngressOwnerReferences("not-gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "should delete a Certificate if its secret name is not present in the Gateway", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + }, + UID: types.UID("gateway-name"), + }, + }, + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + ExpectedEvents: []string{`Normal DeleteCertificate Successfully deleted unrequired Certificate "existing-crt"`}, + ExpectedDelete: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-crt", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "existing-crt", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + }, + }, + }, + }, + }, + { + Name: "should update a Certificate if is contains a Common Name that is not defined on the Gateway annotations", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + cmapi.IssuerKindAnnotationKey: "Issuer", + cmapi.IssuerGroupAnnotationKey: "cert-manager.io", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }}, + }, + }, + CertificateLister: []runtime.Object{ + &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com-tls", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + CommonName: "example-common-name", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "example-com-tls"`}, + ExpectedUpdate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com-tls", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "if a Gateway contains multiple listeners that specify the same secretName, it should create a single Certificate", + Issuer: acmeIssuer, + IssuerLister: []runtime.Object{acmeIssuer}, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer-name", + cmapi.IssuerKindAnnotationKey: "Issuer", + cmapi.IssuerGroupAnnotationKey: "cert-manager.io", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }, { + Hostname: ptrHostname("www.example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }, { + Hostname: ptrHostname("foo.example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }}, + }, + }, + ExpectedEvents: []string{ + `Normal CreateCertificate Successfully created Certificate "example-com-tls"`, + }, + ExpectedCreate: []*cmapi.Certificate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-com-tls", + Namespace: gen.DefaultTestNamespace, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com", "www.example.com", "foo.example.com"}, + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "Issuer", + Group: "cert-manager.io", + }, + Usages: cmapi.DefaultKeyUsages(), + }, + }, + }, + }, + { + Name: "Failure to translate the Gateway annotations", + Issuer: acmeIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-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("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com-tls", + }, + }, + }}, + }, + }, + Err: true, + }, + { + Name: "return a single Certificate for a Gateway with a single valid TLS entry with common-name and keyusage annotation", + Issuer: acmeClusterIssuer, + IngressLike: &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-name", + Namespace: gen.DefaultTestNamespace, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + Annotations: map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", + cmapi.CommonNameAnnotationKey: "my-cn", + "cert-manager.io/usages": "signing,digital signature,content commitment", + }, + UID: types.UID("gateway-name"), + }, + Spec: gwapi.GatewaySpec{ + GatewayClassName: "test-gateway", + Listeners: []gwapi.Listener{{ + Hostname: ptrHostname("example.com"), + Port: 443, + Protocol: "HTTPS", + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "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, + Labels: map[string]string{ + "my-test-label": "should be copied", + }, + OwnerReferences: buildGatewayOwnerReferences("gateway-name", gen.DefaultTestNamespace), + }, + Spec: cmapi.CertificateSpec{ + DNSNames: []string{"example.com"}, + CommonName: "my-cn", + SecretName: "example-com-tls", + IssuerRef: cmmeta.ObjectReference{ + Name: "issuer-name", + Kind: "ClusterIssuer", + }, + Usages: []cmapi.KeyUsage{ + cmapi.UsageSigning, + cmapi.UsageDigitalSignature, + cmapi.UsageContentCommitment, + }, + }, + }, + }, + }, + } + + testFn := func(test testT) func(t *testing.T) { + return func(t *testing.T) { + var allCMObjects []runtime.Object + allCMObjects = append(allCMObjects, test.IssuerLister...) + allCMObjects = append(allCMObjects, test.ClusterIssuerLister...) + allCMObjects = append(allCMObjects, test.CertificateLister...) + var expectedActions []testpkg.Action + for _, cr := range test.ExpectedCreate { + expectedActions = append(expectedActions, + testpkg.NewAction(coretesting.NewCreateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + cr.Namespace, + cr, + )), + ) + } + for _, cr := range test.ExpectedUpdate { + expectedActions = append(expectedActions, + testpkg.NewAction(coretesting.NewUpdateAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + cr.Namespace, + cr, + )), + ) + } + for _, cr := range test.ExpectedDelete { + expectedActions = append(expectedActions, + testpkg.NewAction(coretesting.NewDeleteAction( + cmapi.SchemeGroupVersion.WithResource("certificates"), + cr.Namespace, + cr.Name, + ))) + } + b := &testpkg.Builder{ + T: t, + CertManagerObjects: allCMObjects, + ExpectedActions: expectedActions, + ExpectedEvents: test.ExpectedEvents, + } + b.Init() + defer b.Stop() + sync := SyncFnFor(b.Recorder, logr.DiscardLogger{}, b.CMClient, b.SharedInformerFactory.Certmanager().V1().Certificates().Lister(), controller.IngressShimOptions{ + DefaultIssuerName: test.DefaultIssuerName, + DefaultIssuerKind: test.DefaultIssuerKind, + DefaultIssuerGroup: test.DefaultIssuerGroup, + DefaultAutoCertificateAnnotations: []string{"kubernetes.io/tls-acme"}, + }) + b.Start() + + err := sync(context.Background(), test.IngressLike) + + // If test.Err == true, err should not be nil and vice versa + if test.Err == (err == nil) { + t.Errorf("Expected error: %v, but got: %v", test.Err, err) + } + + if err := b.AllEventsCalled(); err != nil { + t.Error(err) + } + if err := b.AllReactorsCalled(); err != nil { + t.Errorf("Not all expected reactors were called: %v", err) + } + if err := b.AllActionsExecuted(); err != nil { + t.Errorf(err.Error()) + } + } + } + t.Run("ingress-shim", func(t *testing.T) { + for _, test := range testIngressShim { + t.Run(test.Name, testFn(test)) + } + }) + + t.Run("gateway-shim", func(t *testing.T) { + for _, test := range testGatewayShim { + t.Run(test.Name, testFn(test)) + } + }) + +} + +type fakeHelper struct { + issuer cmapi.GenericIssuer +} + +func (f *fakeHelper) GetGenericIssuer(ref cmmeta.ObjectReference, ns string) (cmapi.GenericIssuer, error) { + if f.issuer == nil { + return nil, fmt.Errorf("no issuer specified on fake helper") + } + return f.issuer, nil +} + +func TestIssuerForIngress(t *testing.T) { + type testT struct { + Ingress *networkingv1beta1.Ingress + DefaultName string + DefaultKind string + DefaultGroup string + ExpectedName string + ExpectedKind string + ExpectedGroup string + ExpectedError error + } + tests := []testT{ + { + Ingress: buildIngress("name", "namespace", map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "issuer", + cmapi.IssuerGroupAnnotationKey: "foo.bar", + }), + DefaultKind: "Issuer", + ExpectedName: "issuer", + ExpectedKind: "Issuer", + ExpectedGroup: "foo.bar", + }, + { + Ingress: buildIngress("name", "namespace", map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "clusterissuer", + }), + ExpectedName: "clusterissuer", + ExpectedKind: "ClusterIssuer", + }, + { + Ingress: buildIngress("name", "namespace", map[string]string{ + "kubernetes.io/tls-acme": "true", + }), + DefaultName: "default-name", + DefaultKind: "ClusterIssuer", + DefaultGroup: "cert-manager.io", + ExpectedName: "default-name", + ExpectedKind: "ClusterIssuer", + ExpectedGroup: "cert-manager.io", + }, + { + Ingress: buildIngress("name", "namespace", nil), + ExpectedError: errors.New("failed to determine issuer name to be used for ingress resource"), + }, + { + Ingress: buildIngress("name", "namespace", map[string]string{ + "kubernetes.io/tls-acme": "true", + }), + ExpectedError: errors.New("failed to determine issuer name to be used for ingress resource"), + }, + { + Ingress: buildIngress("name", "namespace", map[string]string{ + cmapi.IngressClusterIssuerNameAnnotationKey: "clusterissuer", + cmapi.IngressIssuerNameAnnotationKey: "issuer", + cmapi.IssuerGroupAnnotationKey: "group.io", + }), + ExpectedError: errors.New(`both "cert-manager.io/issuer" and "cert-manager.io/cluster-issuer" may not be set, both "cert-manager.io/cluster-issuer" and "cert-manager.io/issuer-group" may not be set`), + }, + } + for _, test := range tests { + defaults := controllerpkg.IngressShimOptions{ + DefaultIssuerKind: test.DefaultKind, + DefaultIssuerName: test.DefaultName, + DefaultIssuerGroup: test.DefaultGroup, + } + name, kind, group, err := issuerForIngressLike(defaults, test.Ingress) + if err != nil { + if test.ExpectedError == nil || err.Error() != test.ExpectedError.Error() { + t.Errorf("unexpected error, exp=%v got=%s", test.ExpectedError, err) + } + } else if test.ExpectedError != nil { + t.Errorf("expected error but got nil: %s", test.ExpectedError) + } + + if name != test.ExpectedName { + t.Errorf("expected name to be %q but got %q", test.ExpectedName, name) + } + + if kind != test.ExpectedKind { + t.Errorf("expected kind to be %q but got %q", test.ExpectedKind, kind) + } + + if group != test.ExpectedGroup { + t.Errorf("expected group to be %q but got %q", test.ExpectedGroup, group) + } + } +} + +func buildCertificate(name, namespace string, ownerReferences []metav1.OwnerReference) *cmapi.Certificate { + return &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + OwnerReferences: ownerReferences, + }, + Spec: cmapi.CertificateSpec{ + SecretName: name, + }, + } +} + +func buildIngress(name, namespace string, annotations map[string]string) *networkingv1beta1.Ingress { + return &networkingv1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: annotations, + UID: types.UID(name), + }, + } +} + +func buildGateway(name, namespace string, annotations map[string]string) *gwapi.Gateway { + return &gwapi.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: annotations, + }, + } +} + +func buildIngressOwnerReferences(name, namespace string) []metav1.OwnerReference { + return []metav1.OwnerReference{ + *metav1.NewControllerRef(buildIngress(name, namespace, nil), ingressGVK), + } +} + +func buildGatewayOwnerReferences(name, namespace string) []metav1.OwnerReference { + return []metav1.OwnerReference{ + *metav1.NewControllerRef(buildIngress(name, namespace, nil), gatewayGVK), + } +} + +func ptrHostname(hostname string) *gwapi.Hostname { + h := gwapi.Hostname(hostname) + return &h +} + +func ptrMode(mode gwapi.TLSModeType) *gwapi.TLSModeType { + return &mode +} + +func Test_validateGatewayListenerBlock(t *testing.T) { + tests := []struct { + name string + listener gwapi.Listener + wantErr string + }{ + { + name: "empty TLS block", + listener: gwapi.Listener{ + Hostname: ptrHostname("example.com"), + Port: gwapi.PortNumber(443), + Protocol: gwapi.HTTPSProtocolType, + }, + wantErr: "spec.listeners[0].tls: Required value: the TLS block cannot be empty", + }, + { + name: "empty hostname", + listener: gwapi.Listener{ + Hostname: ptrHostname(""), + Port: gwapi.PortNumber(443), + Protocol: gwapi.HTTPSProtocolType, + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "Secret", + Name: "example-com", + }, + }, + }, + wantErr: "spec.listeners[0].hostname: Required value: the hostname cannot be empty", + }, + { + name: "empty group", + listener: gwapi.Listener{ + Hostname: ptrHostname("example.com"), + Port: gwapi.PortNumber(443), + Protocol: gwapi.HTTPSProtocolType, + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "", + Kind: "Secret", + Name: "example-com", + }, + }, + }, + wantErr: "spec.listeners[0].tls.certificateRef.group: Unsupported value: \"\": supported values: \"core\"", + }, + { + name: "unsupported group", + listener: gwapi.Listener{ + Hostname: ptrHostname("example.com"), + Port: gwapi.PortNumber(443), + Protocol: gwapi.HTTPSProtocolType, + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "invalid", + Kind: "Secret", + Name: "example-com", + }, + }, + }, + wantErr: "spec.listeners[0].tls.certificateRef.group: Unsupported value: \"invalid\": supported values: \"core\"", + }, + { + name: "unsupported kind", + listener: gwapi.Listener{ + Hostname: ptrHostname("example.com"), + Port: gwapi.PortNumber(443), + Protocol: gwapi.HTTPSProtocolType, + TLS: &gwapi.GatewayTLSConfig{ + Mode: ptrMode(gwapi.TLSModeTerminate), + CertificateRef: &gwapi.LocalObjectReference{ + Group: "core", + Kind: "SomeOtherKind", + Name: "example-com", + }, + }, + }, + wantErr: "spec.listeners[0].tls.certificateRef.kind: Unsupported value: \"SomeOtherKind\": supported values: \"Secret\"", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotErr := validateGatewayListenerBlock(field.NewPath("spec", "listeners").Index(0), test.listener).ToAggregate() + if test.wantErr == "" { + assert.NoError(t, gotErr) + } else { + assert.EqualError(t, gotErr, test.wantErr) + } + }) + } +} diff --git a/pkg/controller/context.go b/pkg/controller/context.go index 05f9cde92..956908e81 100644 --- a/pkg/controller/context.go +++ b/pkg/controller/context.go @@ -26,6 +26,8 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "k8s.io/utils/clock" + gwclient "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" + gwinformers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions" "github.com/jetstack/cert-manager/pkg/acme/accounts" clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned" @@ -50,6 +52,9 @@ type Context struct { Client kubernetes.Interface // CMClient is a cert-manager clientset CMClient clientset.Interface + // GWClient is a GatewayAPI clientset. + GWClient gwclient.Interface + // Recorder to record events to Recorder record.EventRecorder @@ -60,6 +65,10 @@ type Context struct { // instances SharedInformerFactory informers.SharedInformerFactory + // The Gateway API is an external CRD, which means its shared informers are + // not available in controllerpkg.Context. + GWShared gwinformers.SharedInformerFactory + // Namespace is the namespace to operate within. // If unset, operates on all namespaces Namespace string @@ -126,8 +135,10 @@ type ACMEOptions struct { DNS01CheckRetryPeriod time.Duration } +// IngressShimOptions contain default Issuer GVK config for the certificate-shim controllers. +// These are set from the cmd cli flags, allowing the controllers to support legacy annotations +// such as `kubernetes.io/tls-acme`. type IngressShimOptions struct { - // Default issuer/certificates details consumed by ingress-shim DefaultIssuerName string DefaultIssuerKind string DefaultIssuerGroup string diff --git a/pkg/controller/ingress-shim/sync.go b/pkg/controller/ingress-shim/sync.go deleted file mode 100644 index c4d435f73..000000000 --- a/pkg/controller/ingress-shim/sync.go +++ /dev/null @@ -1,408 +0,0 @@ -/* -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 ( - "context" - "errors" - "fmt" - "reflect" - "strconv" - "strings" - - corev1 "k8s.io/api/core/v1" - networkingv1beta1 "k8s.io/api/networking/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - - cmacme "github.com/jetstack/cert-manager/pkg/apis/acme/v1" - cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" - cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" - logf "github.com/jetstack/cert-manager/pkg/logs" - utilerrors "k8s.io/apimachinery/pkg/util/errors" -) - -const ( - reasonBadConfig = "BadConfig" - reasonCreateCertificate = "CreateCertificate" - reasonUpdateCertificate = "UpdateCertificate" - reasonDeleteCertificate = "DeleteCertificate" -) - -var ingressGVK = networkingv1beta1.SchemeGroupVersion.WithKind("Ingress") - -func (c *controller) sync(ctx context.Context, ing *networkingv1beta1.Ingress) error { - log := logf.WithResource(logf.FromContext(ctx), ing) - ctx = logf.NewContext(ctx, log) - - if !shouldSync(ing, c.defaults.autoCertificateAnnotations) { - logf.V(logf.DebugLevel).Infof("not syncing ingress resource as it does not contain a %q or %q annotation", - cmapi.IngressIssuerNameAnnotationKey, cmapi.IngressClusterIssuerNameAnnotationKey) - return nil - } - - issuerName, issuerKind, issuerGroup, err := c.issuerForIngress(ing) - if err != nil { - log.Error(err, "failed to determine issuer to be used for ingress resource") - c.recorder.Eventf(ing, corev1.EventTypeWarning, reasonBadConfig, "Could not determine issuer for ingress due to bad annotations: %s", - err) - return nil - } - - errs := c.validateIngress(ing) - if len(errs) > 0 { - errMsg := errs[0].Error() - if len(errs) > 1 { - errMsg = utilerrors.NewAggregate(errs).Error() - } - c.recorder.Eventf(ing, corev1.EventTypeWarning, reasonBadConfig, errMsg) - return nil - } - - newCrts, updateCrts, err := c.buildCertificates(ctx, ing, issuerName, issuerKind, issuerGroup) - if err != nil { - return err - } - - for _, crt := range newCrts { - _, err := c.cmClient.CertmanagerV1().Certificates(crt.Namespace).Create(ctx, crt, metav1.CreateOptions{}) - if err != nil { - return err - } - c.recorder.Eventf(ing, corev1.EventTypeNormal, reasonCreateCertificate, "Successfully created Certificate %q", crt.Name) - } - - for _, crt := range updateCrts { - _, err := c.cmClient.CertmanagerV1().Certificates(crt.Namespace).Update(ctx, crt, metav1.UpdateOptions{}) - if err != nil { - return err - } - c.recorder.Eventf(ing, corev1.EventTypeNormal, reasonUpdateCertificate, "Successfully updated Certificate %q", crt.Name) - } - - unrequiredCrts, err := c.findUnrequiredCertificates(ing) - if err != nil { - return err - } - - for _, crt := range unrequiredCrts { - err = c.cmClient.CertmanagerV1().Certificates(crt.Namespace).Delete(ctx, crt.Name, metav1.DeleteOptions{}) - if err != nil { - return err - } - c.recorder.Eventf(ing, corev1.EventTypeNormal, reasonDeleteCertificate, "Successfully deleted unrequired Certificate %q", crt.Name) - } - - return nil -} - -func (c *controller) validateIngress(ing *networkingv1beta1.Ingress) []error { - // check for duplicate values of networkingv1beta1.IngressTLS.SecretName - var errs []error - namedSecrets := make(map[string]int) - for _, tls := range ing.Spec.TLS { - namedSecrets[tls.SecretName]++ - } - // not doing this in the previous for-loop to avoid erroring more than once for the same SecretName - for name, n := range namedSecrets { - if n > 1 { - errs = append(errs, fmt.Errorf("Duplicate TLS entry for secretName %q", name)) - } - } - return errs -} - -func validateIngressTLSBlock(tlsBlock networkingv1beta1.IngressTLS) []error { - // unlikely that _both_ SecretName and Hosts would be empty, but still returning []error for consistency - var errs []error - - if len(tlsBlock.Hosts) == 0 { - errs = append(errs, fmt.Errorf("secret %q for ingress TLS has no hosts specified", tlsBlock.SecretName)) - } - if tlsBlock.SecretName == "" { - errs = append(errs, fmt.Errorf("TLS entry for hosts %v must specify a secretName", tlsBlock.Hosts)) - } - return errs -} - -func (c *controller) buildCertificates(ctx context.Context, ing *networkingv1beta1.Ingress, - issuerName, issuerKind, issuerGroup string) (new, update []*cmapi.Certificate, _ error) { - log := logf.FromContext(ctx) - - var newCrts []*cmapi.Certificate - var updateCrts []*cmapi.Certificate - for i, tls := range ing.Spec.TLS { - errs := validateIngressTLSBlock(tls) - // if this tls entry is invalid, record an error event on Ingress object and continue to the next tls entry - if len(errs) > 0 { - errMsg := utilerrors.NewAggregate(errs).Error() - c.recorder.Eventf(ing, corev1.EventTypeWarning, reasonBadConfig, fmt.Sprintf("TLS entry %d is invalid: %s", i, errMsg)) - continue - } - existingCrt, err := c.certificateLister.Certificates(ing.Namespace).Get(tls.SecretName) - if !apierrors.IsNotFound(err) && err != nil { - return nil, nil, err - } - - crt := &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: tls.SecretName, - Namespace: ing.Namespace, - Labels: ing.Labels, - OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ing, ingressGVK)}, - }, - Spec: cmapi.CertificateSpec{ - DNSNames: tls.Hosts, - SecretName: tls.SecretName, - IssuerRef: cmmeta.ObjectReference{ - Name: issuerName, - Kind: issuerKind, - Group: issuerGroup, - }, - Usages: cmapi.DefaultKeyUsages(), - }, - } - - setIssuerSpecificConfig(crt, ing) - if err := translateIngressAnnotations(crt, ing.Annotations); err != nil { - return nil, nil, err - } - - // check if a Certificate for this TLS entry already exists, and if it - // does then skip this entry - if existingCrt != nil { - log := logf.WithRelatedResource(log, existingCrt) - log.V(logf.DebugLevel).Info("certificate already exists for ingress resource, ensuring it is up to date") - - if metav1.GetControllerOf(existingCrt) == nil { - log.V(logf.InfoLevel).Info("certificate resource has no owner. refusing to update non-owned certificate resource for ingress") - continue - } - - if !metav1.IsControlledBy(existingCrt, ing) { - log.V(logf.InfoLevel).Info("certificate resource is not owned by this ingress. refusing to update non-owned certificate resource for ingress") - continue - } - - if !certNeedsUpdate(existingCrt, crt) { - log.V(logf.DebugLevel).Info("certificate resource is already up to date for ingress") - continue - } - - updateCrt := existingCrt.DeepCopy() - - updateCrt.Spec = crt.Spec - updateCrt.Labels = crt.Labels - setIssuerSpecificConfig(updateCrt, ing) - updateCrts = append(updateCrts, updateCrt) - } else { - newCrts = append(newCrts, crt) - } - } - return newCrts, updateCrts, nil -} - -func (c *controller) findUnrequiredCertificates(ing *networkingv1beta1.Ingress) ([]*cmapi.Certificate, error) { - var unrequired []*cmapi.Certificate - // TODO: investigate selector which filters for certificates controlled by the ingress - crts, err := c.certificateLister.Certificates(ing.Namespace).List(labels.Everything()) - if err != nil { - return nil, err - } - - for _, crt := range crts { - if isUnrequiredCertificate(crt, ing) { - unrequired = append(unrequired, crt) - } - } - - return unrequired, nil -} - -func isUnrequiredCertificate(crt *cmapi.Certificate, ing *networkingv1beta1.Ingress) bool { - if !metav1.IsControlledBy(crt, ing) { - return false - } - - for _, tls := range ing.Spec.TLS { - if crt.Spec.SecretName == tls.SecretName { - return false - } - } - return true -} - -// certNeedsUpdate checks and returns true if two Certificates differ -func certNeedsUpdate(a, b *cmapi.Certificate) bool { - if a.Name != b.Name { - return true - } - - // TODO: we may need to allow users to edit the managed Certificate resources - // to add their own labels directly. - // Right now, we'll reset/remove the label values back automatically. - // Let's hope no other controllers do this automatically, else we'll start fighting... - if !reflect.DeepEqual(a.Labels, b.Labels) { - return true - } - - if a.Spec.CommonName != b.Spec.CommonName { - return true - } - - if len(a.Spec.DNSNames) != len(b.Spec.DNSNames) { - return true - } - - for i := range a.Spec.DNSNames { - if a.Spec.DNSNames[i] != b.Spec.DNSNames[i] { - return true - } - } - - if a.Spec.SecretName != b.Spec.SecretName { - return true - } - - if a.Spec.IssuerRef.Name != b.Spec.IssuerRef.Name { - return true - } - - if a.Spec.IssuerRef.Kind != b.Spec.IssuerRef.Kind { - return true - } - - return false -} - -func setIssuerSpecificConfig(crt *cmapi.Certificate, ing *networkingv1beta1.Ingress) { - ingAnnotations := ing.Annotations - if ingAnnotations == nil { - ingAnnotations = map[string]string{} - } - - // for ACME issuers - editInPlaceVal := ingAnnotations[cmacme.IngressEditInPlaceAnnotationKey] - editInPlace := editInPlaceVal == "true" - if editInPlace { - if crt.Annotations == nil { - crt.Annotations = make(map[string]string) - } - crt.Annotations[cmacme.ACMECertificateHTTP01IngressNameOverride] = ing.Name - // set IssueTemporaryCertificateAnnotation to true in order to behave - // better when ingress-gce is being used. - crt.Annotations[cmapi.IssueTemporaryCertificateAnnotation] = "true" - } - - ingressClassVal, hasIngressClassVal := ingAnnotations[cmapi.IngressACMEIssuerHTTP01IngressClassAnnotationKey] - if hasIngressClassVal { - if crt.Annotations == nil { - crt.Annotations = make(map[string]string) - } - crt.Annotations[cmacme.ACMECertificateHTTP01IngressClassOverride] = ingressClassVal - } -} - -// shouldSync returns true if this ingress should have a Certificate resource -// created for it -func shouldSync(ing *networkingv1beta1.Ingress, autoCertificateAnnotations []string) bool { - annotations := ing.Annotations - if annotations == nil { - annotations = map[string]string{} - } - if _, ok := annotations[cmapi.IngressIssuerNameAnnotationKey]; ok { - return true - } - if _, ok := annotations[cmapi.IngressClusterIssuerNameAnnotationKey]; ok { - return true - } - for _, x := range autoCertificateAnnotations { - if s, ok := annotations[x]; ok { - if b, _ := strconv.ParseBool(s); b { - return true - } - } - } - return false -} - -// issuerForIngress will determine the issuer that should be specified on a -// Certificate created for the given Ingress resource. If one is not set, the -// default issuer given to the controller will be used. -func (c *controller) issuerForIngress(ing *networkingv1beta1.Ingress) (name, kind, group string, err error) { - var errs []string - - name = c.defaults.issuerName - kind = c.defaults.issuerKind - group = c.defaults.issuerGroup - annotations := ing.Annotations - - if annotations == nil { - annotations = map[string]string{} - } - - issuerName, issuerNameOK := annotations[cmapi.IngressIssuerNameAnnotationKey] - if issuerNameOK { - name = issuerName - kind = cmapi.IssuerKind - } - - clusterIssuerName, clusterIssuerNameOK := annotations[cmapi.IngressClusterIssuerNameAnnotationKey] - if clusterIssuerNameOK { - name = clusterIssuerName - kind = cmapi.ClusterIssuerKind - } - - kindName, kindNameOK := annotations[cmapi.IssuerKindAnnotationKey] - if kindNameOK { - kind = kindName - } - - groupName, groupNameOK := annotations[cmapi.IssuerGroupAnnotationKey] - if groupNameOK { - group = groupName - } - - if len(name) == 0 { - errs = append(errs, "failed to determine issuer name to be used for ingress resource") - } - - if issuerNameOK && clusterIssuerNameOK { - errs = append(errs, - fmt.Sprintf("both %q and %q may not be set", - cmapi.IngressIssuerNameAnnotationKey, cmapi.IngressClusterIssuerNameAnnotationKey)) - } - - if clusterIssuerNameOK && groupNameOK { - errs = append(errs, - fmt.Sprintf("both %q and %q may not be set", - cmapi.IngressClusterIssuerNameAnnotationKey, cmapi.IssuerGroupAnnotationKey)) - } - - if clusterIssuerNameOK && kindNameOK { - errs = append(errs, - fmt.Sprintf("both %q and %q may not be set", - cmapi.IngressClusterIssuerNameAnnotationKey, cmapi.IssuerKindAnnotationKey)) - } - - if len(errs) > 0 { - return "", "", "", errors.New(strings.Join(errs, ", ")) - } - - return name, kind, group, nil -} diff --git a/pkg/controller/ingress-shim/sync_test.go b/pkg/controller/ingress-shim/sync_test.go deleted file mode 100644 index 022998068..000000000 --- a/pkg/controller/ingress-shim/sync_test.go +++ /dev/null @@ -1,1305 +0,0 @@ -/* -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 ( - "context" - "errors" - "testing" - - networkingv1beta1 "k8s.io/api/networking/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - coretesting "k8s.io/client-go/testing" - - cmacme "github.com/jetstack/cert-manager/pkg/apis/acme/v1" - cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" - cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" - testpkg "github.com/jetstack/cert-manager/pkg/controller/test" - "github.com/jetstack/cert-manager/test/unit/gen" -) - -const testAcmeTLSAnnotation = "kubernetes.io/tls-acme" - -func TestShouldSync(t *testing.T) { - type testT struct { - Annotations map[string]string - ShouldSync bool - } - tests := []testT{ - { - Annotations: map[string]string{cmapi.IngressIssuerNameAnnotationKey: ""}, - ShouldSync: true, - }, - { - Annotations: map[string]string{cmapi.IngressClusterIssuerNameAnnotationKey: ""}, - ShouldSync: true, - }, - { - Annotations: map[string]string{testAcmeTLSAnnotation: "true"}, - ShouldSync: true, - }, - { - Annotations: map[string]string{testAcmeTLSAnnotation: "false"}, - ShouldSync: false, - }, - { - Annotations: map[string]string{testAcmeTLSAnnotation: ""}, - ShouldSync: false, - }, - { - ShouldSync: false, - }, - } - for _, test := range tests { - shouldSync := shouldSync(buildIngress("", "", test.Annotations), []string{"kubernetes.io/tls-acme"}) - if shouldSync != test.ShouldSync { - t.Errorf("Expected shouldSync=%v for annotations %#v", test.ShouldSync, test.Annotations) - } - } -} - -func TestSync(t *testing.T) { - clusterIssuer := gen.ClusterIssuer("issuer-name") - acmeIssuerNewFormat := gen.Issuer("issuer-name", - gen.SetIssuerACME(cmacme.ACMEIssuer{})) - acmeIssuer := gen.Issuer("issuer-name", - gen.SetIssuerACME(cmacme.ACMEIssuer{})) - acmeClusterIssuer := gen.ClusterIssuer("issuer-name", - gen.SetIssuerACME(cmacme.ACMEIssuer{})) - type testT struct { - Name string - Ingress *networkingv1beta1.Ingress - Issuer cmapi.GenericIssuer - IssuerLister []runtime.Object - ClusterIssuerLister []runtime.Object - CertificateLister []runtime.Object - DefaultIssuerName string - DefaultIssuerKind string - DefaultIssuerGroup string - Err bool - ExpectedCreate []*cmapi.Certificate - ExpectedUpdate []*cmapi.Certificate - ExpectedDelete []*cmapi.Certificate - ExpectedEvents []string - } - tests := []testT{ - { - Name: "return a single Certificate for an ingress with a single valid TLS entry and common-name annotation", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - cmapi.CommonNameAnnotationKey: "my-cn", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - CommonName: "my-cn", - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations using edit-in-place", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - cmacme.IngressEditInPlaceAnnotationKey: "true", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - Annotations: map[string]string{ - cmacme.ACMECertificateHTTP01IngressNameOverride: "ingress-name", - cmapi.IssueTemporaryCertificateAnnotation: "true", - }, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "create a Certificate with the HTTP01 name override if the given ingress uses http01 annotations", - Issuer: gen.Issuer(acmeIssuer.Name), - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - cmacme.IngressEditInPlaceAnnotationKey: "true", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - Annotations: map[string]string{ - cmacme.ACMECertificateHTTP01IngressNameOverride: "ingress-name", - cmapi.IssueTemporaryCertificateAnnotation: "true", - }, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations with no ingress class set", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations with a custom ingress class", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - cmapi.IngressClassAnnotationKey: "nginx-ing", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "return a single HTTP01 Certificate for an ingress with a single valid TLS entry and HTTP01 annotations with a certificate ingress class", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - cmapi.IngressACMEIssuerHTTP01IngressClassAnnotationKey: "cert-ing", - cmapi.IngressClassAnnotationKey: "nginx-ing", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - Annotations: map[string]string{ - cmacme.ACMECertificateHTTP01IngressClassOverride: "cert-ing", - }, - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "edit-in-place set to false should not trigger editing the ingress in-place", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - cmapi.IngressClassAnnotationKey: "nginx-ing", - cmacme.IngressEditInPlaceAnnotationKey: "false", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "return a single DNS01 Certificate for an ingress with a single valid TLS entry", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "should return a basic certificate when no provider specific config is provided", - Issuer: clusterIssuer, - DefaultIssuerName: "issuer-name", - DefaultIssuerKind: "ClusterIssuer", - DefaultIssuerGroup: "cert-manager.io", - ClusterIssuerLister: []runtime.Object{clusterIssuer}, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - testAcmeTLSAnnotation: "true", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - }, - }, - }, - }, - ExpectedEvents: []string{`Normal CreateCertificate Successfully created Certificate "example-com-tls"`}, - ExpectedCreate: []*cmapi.Certificate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "example-com-tls", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - Group: "cert-manager.io", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "should skip an invalid TLS entry (no TLS hosts specified)", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - ExpectedEvents: []string{ - `Warning BadConfig TLS entry 0 is invalid: secret "example-com-tls-invalid" for ingress TLS has no hosts specified`, - `Normal CreateCertificate Successfully created Certificate "example-com-tls"`, - }, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - SecretName: "example-com-tls-invalid", - }, - { - SecretName: "example-com-tls", - Hosts: []string{"example.com", "www.example.com"}, - }, - }, - }, - }, - ExpectedCreate: []*cmapi.Certificate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "example-com-tls", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - Usages: cmapi.DefaultKeyUsages(), - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - }, - }, - }, - }, - - { - Name: "should skip an invalid TLS entry (no TLS secret name specified)", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - ExpectedEvents: []string{ - `Warning BadConfig TLS entry 0 is invalid: TLS entry for hosts [example.com] must specify a secretName`, - `Normal CreateCertificate Successfully created Certificate "example-com-tls"`, - }, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - }, - { - Hosts: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - }, - }, - }, - }, - ExpectedCreate: []*cmapi.Certificate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "example-com-tls", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - SecretName: "example-com-tls", - Usages: cmapi.DefaultKeyUsages(), - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - }, - }, - }, - }, - { - Name: "should error if the specified issuer is not found", - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "invalid-issuer-name", - }, - UID: types.UID("ingress-name"), - }, - }, - }, - { - Name: "should not return any certificates if a correct Certificate already exists", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - SecretName: "existing-crt", - }, - }, - }, - }, - DefaultIssuerKind: "Issuer", - DefaultIssuerGroup: "cert-manager.io", - CertificateLister: []runtime.Object{ - &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: "existing-crt", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "existing-crt", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - Group: "cert-manager.io", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "should update a certificate if an incorrect Certificate exists", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - SecretName: "existing-crt", - }, - }, - }, - }, - CertificateLister: []runtime.Object{ - buildCertificate("existing-crt", - gen.DefaultTestNamespace, - buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - ), - }, - DefaultIssuerKind: "Issuer", - ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "existing-crt"`}, - ExpectedUpdate: []*cmapi.Certificate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "existing-crt", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "existing-crt", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "should update an existing Certificate resource with new labels if they do not match those specified on the Ingress", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuerNewFormat}, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - SecretName: "cert-secret-name", - }, - }, - }, - }, - DefaultIssuerKind: "Issuer", - CertificateLister: []runtime.Object{ - &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cert-secret-name", - Namespace: gen.DefaultTestNamespace, - Labels: map[string]string{ - "a-different-value": "should be removed", - }, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "cert-secret-name", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "cert-secret-name"`}, - ExpectedUpdate: []*cmapi.Certificate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "cert-secret-name", - Namespace: gen.DefaultTestNamespace, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "cert-secret-name", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "should not update certificate if it does not belong to any ingress", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - cmapi.IngressClassAnnotationKey: "toot-ing", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - SecretName: "existing-crt", - }, - }, - }, - }, - CertificateLister: []runtime.Object{ - &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: "existing-crt", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: []metav1.OwnerReference{}, - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "existing-crt", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "should not update certificate if it does not belong to the ingress", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - cmapi.IngressClassAnnotationKey: "toot-ing", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - SecretName: "existing-crt", - }, - }, - }, - }, - CertificateLister: []runtime.Object{ - &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: "existing-crt", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("not-ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "existing-crt", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "should delete a Certificate if its SecretName is not present in the ingress", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Annotations: map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer-name", - }, - UID: types.UID("ingress-name"), - }, - }, - CertificateLister: []runtime.Object{ - &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: "existing-crt", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "existing-crt", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - ExpectedEvents: []string{`Normal DeleteCertificate Successfully deleted unrequired Certificate "existing-crt"`}, - ExpectedDelete: []*cmapi.Certificate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "existing-crt", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "existing-crt", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - }, - }, - }, - }, - }, - { - Name: "should update a Certificate if is contains a Common Name that is not defined on the ingress annotations", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{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", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - SecretName: "example-com-tls", - }, - }, - }, - }, - CertificateLister: []runtime.Object{ - &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-com-tls", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "example-com-tls", - CommonName: "example-common-name", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - Group: "cert-manager.io", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - ExpectedEvents: []string{`Normal UpdateCertificate Successfully updated Certificate "example-com-tls"`}, - ExpectedUpdate: []*cmapi.Certificate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "example-com-tls", - Namespace: gen.DefaultTestNamespace, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com"}, - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "Issuer", - Group: "cert-manager.io", - }, - Usages: cmapi.DefaultKeyUsages(), - }, - }, - }, - }, - { - Name: "if an ingress contains multiple tls entries that specify the same secretName, an error should be logged and no action taken", - Issuer: acmeIssuer, - IssuerLister: []runtime.Object{acmeIssuer}, - ExpectedEvents: []string{ - `Warning BadConfig Duplicate TLS entry for secretName "example-com-tls"`, - }, - 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", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.IngressTLS{ - { - Hosts: []string{"example.com"}, - SecretName: "example-com-tls", - }, - { - Hosts: []string{"notexample.com"}, - SecretName: "example-com-tls", - }, - }, - }, - }, - }, - { - 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, - }, - { - Name: "return a single Certificate for an ingress with a single valid TLS entry with common-name and keyusage annotation", - Issuer: acmeClusterIssuer, - Ingress: &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-name", - Namespace: gen.DefaultTestNamespace, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - Annotations: map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "issuer-name", - cmapi.CommonNameAnnotationKey: "my-cn", - "cert-manager.io/usages": "signing,digital signature,content commitment", - }, - UID: types.UID("ingress-name"), - }, - Spec: networkingv1beta1.IngressSpec{ - TLS: []networkingv1beta1.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, - Labels: map[string]string{ - "my-test-label": "should be copied", - }, - OwnerReferences: buildOwnerReferences("ingress-name", gen.DefaultTestNamespace), - }, - Spec: cmapi.CertificateSpec{ - DNSNames: []string{"example.com", "www.example.com"}, - CommonName: "my-cn", - SecretName: "example-com-tls", - IssuerRef: cmmeta.ObjectReference{ - Name: "issuer-name", - Kind: "ClusterIssuer", - }, - Usages: []cmapi.KeyUsage{ - cmapi.UsageSigning, - cmapi.UsageDigitalSignature, - cmapi.UsageContentCommitment, - }, - }, - }, - }, - }, - } - testFn := func(test testT) func(t *testing.T) { - return func(t *testing.T) { - var allCMObjects []runtime.Object - allCMObjects = append(allCMObjects, test.IssuerLister...) - allCMObjects = append(allCMObjects, test.ClusterIssuerLister...) - allCMObjects = append(allCMObjects, test.CertificateLister...) - var expectedActions []testpkg.Action - for _, cr := range test.ExpectedCreate { - expectedActions = append(expectedActions, - testpkg.NewAction(coretesting.NewCreateAction( - cmapi.SchemeGroupVersion.WithResource("certificates"), - cr.Namespace, - cr, - )), - ) - } - for _, cr := range test.ExpectedUpdate { - expectedActions = append(expectedActions, - testpkg.NewAction(coretesting.NewUpdateAction( - cmapi.SchemeGroupVersion.WithResource("certificates"), - cr.Namespace, - cr, - )), - ) - } - for _, cr := range test.ExpectedDelete { - expectedActions = append(expectedActions, - testpkg.NewAction(coretesting.NewDeleteAction( - cmapi.SchemeGroupVersion.WithResource("certificates"), - cr.Namespace, - cr.Name, - ))) - } - b := &testpkg.Builder{ - T: t, - CertManagerObjects: allCMObjects, - ExpectedActions: expectedActions, - ExpectedEvents: test.ExpectedEvents, - } - b.Init() - defer b.Stop() - c := &controller{ - kClient: b.Client, - cmClient: b.CMClient, - recorder: b.Recorder, - certificateLister: b.SharedInformerFactory.Certmanager().V1().Certificates().Lister(), - defaults: defaults{ - issuerName: test.DefaultIssuerName, - issuerKind: test.DefaultIssuerKind, - issuerGroup: test.DefaultIssuerGroup, - autoCertificateAnnotations: []string{testAcmeTLSAnnotation}, - }, - } - b.Start() - - err := c.sync(context.Background(), test.Ingress) - - // If test.Err == true, err should not be nil and vice versa - if test.Err == (err == nil) { - t.Errorf("Expected error: %v, but got: %v", test.Err, err) - } - - if err := b.AllEventsCalled(); err != nil { - t.Error(err) - } - if err := b.AllReactorsCalled(); err != nil { - t.Errorf("Not all expected reactors were called: %v", err) - } - if err := b.AllActionsExecuted(); err != nil { - t.Errorf(err.Error()) - } - } - } - for _, test := range tests { - t.Run(test.Name, testFn(test)) - } -} - -func TestIssuerForIngress(t *testing.T) { - type testT struct { - Ingress *networkingv1beta1.Ingress - DefaultName string - DefaultKind string - DefaultGroup string - ExpectedName string - ExpectedKind string - ExpectedGroup string - ExpectedError error - } - tests := []testT{ - { - Ingress: buildIngress("name", "namespace", map[string]string{ - cmapi.IngressIssuerNameAnnotationKey: "issuer", - cmapi.IssuerGroupAnnotationKey: "foo.bar", - }), - DefaultKind: "Issuer", - ExpectedName: "issuer", - ExpectedKind: "Issuer", - ExpectedGroup: "foo.bar", - }, - { - Ingress: buildIngress("name", "namespace", map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "clusterissuer", - }), - ExpectedName: "clusterissuer", - ExpectedKind: "ClusterIssuer", - }, - { - Ingress: buildIngress("name", "namespace", map[string]string{ - testAcmeTLSAnnotation: "true", - }), - DefaultName: "default-name", - DefaultKind: "ClusterIssuer", - DefaultGroup: "cert-manager.io", - ExpectedName: "default-name", - ExpectedKind: "ClusterIssuer", - ExpectedGroup: "cert-manager.io", - }, - { - Ingress: buildIngress("name", "namespace", nil), - ExpectedError: errors.New("failed to determine issuer name to be used for ingress resource"), - }, - { - Ingress: buildIngress("name", "namespace", map[string]string{ - testAcmeTLSAnnotation: "true", - }), - ExpectedError: errors.New("failed to determine issuer name to be used for ingress resource"), - }, - { - Ingress: buildIngress("name", "namespace", map[string]string{ - cmapi.IngressClusterIssuerNameAnnotationKey: "clusterissuer", - cmapi.IngressIssuerNameAnnotationKey: "issuer", - cmapi.IssuerGroupAnnotationKey: "group.io", - }), - ExpectedError: errors.New(`both "cert-manager.io/issuer" and "cert-manager.io/cluster-issuer" may not be set, both "cert-manager.io/cluster-issuer" and "cert-manager.io/issuer-group" may not be set`), - }, - } - for _, test := range tests { - c := &controller{ - defaults: defaults{ - issuerKind: test.DefaultKind, - issuerName: test.DefaultName, - issuerGroup: test.DefaultGroup, - }, - } - name, kind, group, err := c.issuerForIngress(test.Ingress) - if err != nil { - if test.ExpectedError == nil || err.Error() != test.ExpectedError.Error() { - t.Errorf("unexpected error, exp=%v got=%s", test.ExpectedError, err) - } - } else if test.ExpectedError != nil { - t.Errorf("expected error but got nil: %s", test.ExpectedError) - } - - if name != test.ExpectedName { - t.Errorf("expected name to be %q but got %q", test.ExpectedName, name) - } - - if kind != test.ExpectedKind { - t.Errorf("expected kind to be %q but got %q", test.ExpectedKind, kind) - } - - if group != test.ExpectedGroup { - t.Errorf("expected group to be %q but got %q", test.ExpectedGroup, group) - } - } -} - -func buildCertificate(name, namespace string, ownerReferences []metav1.OwnerReference) *cmapi.Certificate { - return &cmapi.Certificate{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - OwnerReferences: ownerReferences, - }, - Spec: cmapi.CertificateSpec{ - SecretName: name, - }, - } -} - -func buildIngress(name, namespace string, annotations map[string]string) *networkingv1beta1.Ingress { - return &networkingv1beta1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Annotations: annotations, - UID: types.UID(name), - }, - } -} - -func buildOwnerReferences(name, namespace string) []metav1.OwnerReference { - return []metav1.OwnerReference{ - *metav1.NewControllerRef(buildIngress(name, namespace, nil), ingressGVK), - } -} diff --git a/pkg/controller/test/BUILD.bazel b/pkg/controller/test/BUILD.bazel index 680a537a6..e0a12e683 100644 --- a/pkg/controller/test/BUILD.bazel +++ b/pkg/controller/test/BUILD.bazel @@ -28,6 +28,8 @@ go_library( "@io_k8s_client_go//kubernetes/fake:go_default_library", "@io_k8s_client_go//testing:go_default_library", "@io_k8s_client_go//tools/cache:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/clientset/versioned/fake:go_default_library", + "@io_k8s_sigs_gateway_api//pkg/client/informers/externalversions:go_default_library", "@io_k8s_utils//clock:go_default_library", "@io_k8s_utils//clock/testing:go_default_library", ], diff --git a/pkg/controller/test/context_builder.go b/pkg/controller/test/context_builder.go index 55c75eec1..89f4f59f5 100644 --- a/pkg/controller/test/context_builder.go +++ b/pkg/controller/test/context_builder.go @@ -33,6 +33,8 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/utils/clock" fakeclock "k8s.io/utils/clock/testing" + gwfake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake" + gwinformers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions" apiutil "github.com/jetstack/cert-manager/pkg/api/util" cmfake "github.com/jetstack/cert-manager/pkg/client/clientset/versioned/fake" @@ -50,14 +52,15 @@ func init() { } // Builder is a structure used to construct new Contexts for use during tests. -// Currently, only KubeObjects and CertManagerObjects can be specified. -// These will be auto loaded into the constructed fake Clientsets. +// Currently, only KubeObjects, CertManagerObjects and GWObjects can be +// specified. These will be auto loaded into the constructed fake Clientsets. // Call ToContext() to construct a new context using the given values. type Builder struct { T *testing.T KubeObjects []runtime.Object CertManagerObjects []runtime.Object + GWObjects []runtime.Object ExpectedActions []Action ExpectedEvents []string StringGenerator StringGenerator @@ -107,12 +110,14 @@ func (b *Builder) Init() { b.requiredReactors = make(map[string]bool) b.Client = kubefake.NewSimpleClientset(b.KubeObjects...) b.CMClient = cmfake.NewSimpleClientset(b.CertManagerObjects...) + b.GWClient = gwfake.NewSimpleClientset(b.GWObjects...) b.Recorder = new(FakeRecorder) - b.FakeKubeClient().PrependReactor("create", "*", b.generateNameReactor) b.FakeCMClient().PrependReactor("create", "*", b.generateNameReactor) + b.FakeGWClient().PrependReactor("create", "*", b.generateNameReactor) b.KubeSharedInformerFactory = kubeinformers.NewSharedInformerFactory(b.Client, informerResyncPeriod) b.SharedInformerFactory = informers.NewSharedInformerFactory(b.CMClient, informerResyncPeriod) + b.GWShared = gwinformers.NewSharedInformerFactory(b.GWClient, informerResyncPeriod) b.stopCh = make(chan struct{}) b.Metrics = metrics.New(logs.Log, clock.RealClock{}) @@ -139,6 +144,10 @@ func (b *Builder) FakeCMClient() *cmfake.Clientset { return b.Context.CMClient.(*cmfake.Clientset) } +func (b *Builder) FakeGWClient() *gwfake.Clientset { + return b.Context.GWClient.(*gwfake.Clientset) +} + func (b *Builder) FakeCMInformerFactory() informers.SharedInformerFactory { return b.Context.SharedInformerFactory } @@ -202,6 +211,7 @@ func (b *Builder) AllEventsCalled() error { func (b *Builder) AllActionsExecuted() error { firedActions := b.FakeCMClient().Actions() firedActions = append(firedActions, b.FakeKubeClient().Actions()...) + firedActions = append(firedActions, b.FakeGWClient().Actions()...) var unexpectedActions []coretesting.Action var errs []error @@ -270,6 +280,8 @@ func (b *Builder) Stop() { func (b *Builder) Start() { b.KubeSharedInformerFactory.Start(b.stopCh) b.SharedInformerFactory.Start(b.stopCh) + b.GWShared.Start(b.stopCh) + // wait for caches to sync b.Sync() } @@ -281,6 +293,9 @@ func (b *Builder) Sync() { if err := mustAllSync(b.SharedInformerFactory.WaitForCacheSync(b.stopCh)); err != nil { panic("Error waiting for SharedInformerFactory to sync: " + err.Error()) } + if err := mustAllSync(b.GWShared.WaitForCacheSync(b.stopCh)); err != nil { + panic("Error waiting for GWShared to sync: " + err.Error()) + } if b.additionalSyncFuncs != nil { cache.WaitForCacheSync(b.stopCh, b.additionalSyncFuncs...) }