From da8bf264fd9de6f184bdb383f4f3dbbadf9180df Mon Sep 17 00:00:00 2001 From: James Munnelly Date: Thu, 17 Oct 2019 00:54:40 +0100 Subject: [PATCH] Use standard golang http server for webhook Signed-off-by: James Munnelly --- LICENSES | 209 ------------- cmd/webhook/BUILD.bazel | 3 +- cmd/webhook/main.go | 109 ++++--- .../templates/webhook-apiservice.yaml | 22 -- .../templates/webhook-deployment.yaml | 10 + .../templates/webhook-mutating-webhook.yaml | 10 +- .../templates/webhook-validating-webhook.yaml | 10 +- go.mod | 3 - go.sum | 2 - hack/build/repos.bzl | 9 - pkg/webhook/BUILD.bazel | 1 + pkg/webhook/handlers/BUILD.bazel | 1 + pkg/webhook/handlers/interfaces.go | 54 ++++ pkg/webhook/server/BUILD.bazel | 37 +++ pkg/webhook/server/server.go | 295 ++++++++++++++++++ pkg/webhook/server/source.go | 41 +++ pkg/webhook/server/tls_file_source.go | 154 +++++++++ 17 files changed, 670 insertions(+), 300 deletions(-) delete mode 100644 deploy/charts/cert-manager/templates/webhook-apiservice.yaml create mode 100644 pkg/webhook/handlers/interfaces.go create mode 100644 pkg/webhook/server/BUILD.bazel create mode 100644 pkg/webhook/server/server.go create mode 100644 pkg/webhook/server/source.go create mode 100644 pkg/webhook/server/tls_file_source.go diff --git a/LICENSES b/LICENSES index a04cb6a6d..923eaa946 100644 --- a/LICENSES +++ b/LICENSES @@ -9526,215 +9526,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -================================================================================ -= vendor/github.com/openshift/generic-admission-server 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 {yyyy} {name of copyright owner} - - 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/github.com/openshift/generic-admission-server/LICENSE e3fc50a88d0a364313df4b21ef20c29e -================================================================================ - - ================================================================================ = vendor/github.com/pborman/uuid licensed under: = diff --git a/cmd/webhook/BUILD.bazel b/cmd/webhook/BUILD.bazel index e76a5bfba..3f7bb1383 100644 --- a/cmd/webhook/BUILD.bazel +++ b/cmd/webhook/BUILD.bazel @@ -18,9 +18,10 @@ go_library( "//pkg/logs:go_default_library", "//pkg/webhook:go_default_library", "//pkg/webhook/handlers:go_default_library", - "@com_github_openshift_generic_admission_server//pkg/cmd:go_default_library", + "//pkg/webhook/server:go_default_library", "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library", "@io_k8s_klog//:go_default_library", + "@io_k8s_klog//klogr:go_default_library", ], ) diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 3fffa7f78..4ad462b14 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -18,23 +18,38 @@ package main import ( "flag" + "fmt" "os" - "time" + "os/signal" + "syscall" - "github.com/openshift/generic-admission-server/pkg/cmd" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog" + "k8s.io/klog/klogr" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" "github.com/jetstack/cert-manager/pkg/logs" "github.com/jetstack/cert-manager/pkg/webhook" "github.com/jetstack/cert-manager/pkg/webhook/handlers" + "github.com/jetstack/cert-manager/pkg/webhook/server" ) var ( GroupName = "webhook." + v1alpha2.SchemeGroupVersion.Group + + securePort int + healthzPort int + tlsCertFile string + tlsKeyFile string ) +func init() { + flag.IntVar(&healthzPort, "healthz-port", 6080, "port number to listen on for insecure healthz connections") + flag.IntVar(&securePort, "secure-port", 6443, "port number to listen on for secure TLS connections") + flag.StringVar(&tlsCertFile, "tls-cert-file", "", "path to the file containing the TLS certificate to serve with") + flag.StringVar(&tlsKeyFile, "tls-private-key-file", "", "path to the file containing the TLS private key to serve with") +} + var ( validationFuncs = map[schema.GroupVersionKind]handlers.ValidationFunc{ v1alpha2.SchemeGroupVersion.WithKind(v1alpha2.CertificateKind): webhook.ValidateCertificate, @@ -44,50 +59,60 @@ var ( } ) -var validationHook cmd.ValidatingAdmissionHook = handlers.NewFuncBackedValidator(logs.Log, GroupName, webhook.Scheme, validationFuncs) -var mutationHook cmd.MutatingAdmissionHook = handlers.NewSchemeBackedDefaulter(logs.Log, GroupName, webhook.Scheme) +var validationHook handlers.ValidatingAdmissionHook = handlers.NewFuncBackedValidator(logs.Log, GroupName, webhook.Scheme, validationFuncs) +var mutationHook handlers.MutatingAdmissionHook = handlers.NewSchemeBackedDefaulter(logs.Log, GroupName, webhook.Scheme) func main() { - // Avoid "logging before flag.Parse" errors from glog - flag.CommandLine.Parse([]string{}) + klog.InitFlags(flag.CommandLine) + flag.Parse() - // parse the command line flags to pull out the tls-cert-file - // argument. This flag will be parsed by code inside cmd.RunAdmissionServer - // so no need to pass it through the call stack or have nice errors - tlsflagSet := flag.NewFlagSet("tls", flag.ContinueOnError) - tlsflagVal := tlsflagSet.String("tls-cert-file", "", "") - tlsflagSet.Parse(os.Args[1:]) - if *tlsflagVal != "" { - runfilewatch(*tlsflagVal) - } + log := klogr.New() + stopCh := setupSignalHandler() - cmd.RunAdmissionServer( - validationHook, - mutationHook, - ) -} - -func runfilewatch(filename string) { - info, err := os.Stat(filename) - if err != nil { - // missing TLS cert file will get turned into a proper error later - return - } - modtime := info.ModTime() - go func() { - for { - time.Sleep(1 * time.Minute) - info, err := os.Stat(filename) - if err != nil { - continue - } - if info.ModTime().After(modtime) { - // let the k8s scheduler restart us - // TODO(dmo): figure out if there's a way to do this with clean - // shutdown - klog.Infof("Detected change in TLS certificate %s. Restarting to pick up new certificate", filename) - os.Exit(0) - } + var source server.CertificateSource + if tlsCertFile == "" || tlsKeyFile == "" { + log.Info("warning: serving insecurely as tls certificate data not provided") + } else { + log.Info("enabling TLS as certificate file flags specified") + source = &server.FileCertificateSource{ + CertPath: tlsCertFile, + KeyPath: tlsKeyFile, + Log: log, } + } + + srv := server.Server{ + ListenAddr: fmt.Sprintf(":%d", securePort), + HealthzAddr: fmt.Sprintf(":%d", healthzPort), + CertificateSource: source, + ValidationWebhook: validationHook, + MutationWebhook: mutationHook, + Log: log, + } + if err := srv.Run(stopCh); err != nil { + log.Error(err, "error running server") + os.Exit(1) + } +} + +var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} +var onlyOneSignalHandler = make(chan struct{}) + +// setupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned +// which is closed on one of these signals. If a second signal is caught, the program +// is terminated with exit code 1. +func setupSignalHandler() (stopCh <-chan struct{}) { + close(onlyOneSignalHandler) // panics when called twice + + stop := make(chan struct{}) + c := make(chan os.Signal, 2) + signal.Notify(c, shutdownSignals...) + go func() { + <-c + close(stop) + <-c + os.Exit(1) // second signal. Exit directly. }() + + return stop } diff --git a/deploy/charts/cert-manager/templates/webhook-apiservice.yaml b/deploy/charts/cert-manager/templates/webhook-apiservice.yaml deleted file mode 100644 index f64e2d43b..000000000 --- a/deploy/charts/cert-manager/templates/webhook-apiservice.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{- if .Values.webhook.enabled -}} -apiVersion: apiregistration.k8s.io/v1beta1 -kind: APIService -metadata: - name: v1beta1.webhook.cert-manager.io - labels: - app: {{ include "webhook.name" . }} - app.kubernetes.io/name: {{ include "webhook.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - helm.sh/chart: {{ include "webhook.chart" . }} - annotations: - cert-manager.io/inject-ca-from-secret: "{{ .Release.Namespace }}/{{ include "webhook.servingCertificate" . }}" -spec: - group: webhook.cert-manager.io - groupPriorityMinimum: 1000 - versionPriority: 15 - service: - name: {{ include "webhook.fullname" . }} - namespace: "{{ .Release.Namespace }}" - version: v1beta1 -{{- end -}} diff --git a/deploy/charts/cert-manager/templates/webhook-deployment.yaml b/deploy/charts/cert-manager/templates/webhook-deployment.yaml index a686f5699..d5eaed7dd 100644 --- a/deploy/charts/cert-manager/templates/webhook-deployment.yaml +++ b/deploy/charts/cert-manager/templates/webhook-deployment.yaml @@ -53,6 +53,16 @@ spec: {{- if .Values.webhook.extraArgs }} {{ toYaml .Values.webhook.extraArgs | indent 10 }} {{- end }} + livenessProbe: + httpGet: + path: /livez + port: 6080 + scheme: HTTP + readinessProbe: + httpGet: + path: /healthz + port: 6080 + scheme: HTTP env: - name: POD_NAMESPACE valueFrom: diff --git a/deploy/charts/cert-manager/templates/webhook-mutating-webhook.yaml b/deploy/charts/cert-manager/templates/webhook-mutating-webhook.yaml index bccd26a8e..3a85d913d 100644 --- a/deploy/charts/cert-manager/templates/webhook-mutating-webhook.yaml +++ b/deploy/charts/cert-manager/templates/webhook-mutating-webhook.yaml @@ -10,9 +10,7 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "webhook.chart" . }} annotations: -{{- if .Values.webhook.injectAPIServerCA }} - cert-manager.io/inject-apiserver-ca: "true" -{{- end }} + cert-manager.io/inject-ca-from-secret: "{{ .Release.Namespace }}/{{ include "webhook.servingCertificate" . }}" webhooks: - name: webhook.cert-manager.io rules: @@ -34,7 +32,7 @@ webhooks: sideEffects: None clientConfig: service: - name: kubernetes - namespace: default - path: /apis/webhook.cert-manager.io/v1beta1/mutations + name: {{ include "webhook.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + path: /mutate {{- end -}} diff --git a/deploy/charts/cert-manager/templates/webhook-validating-webhook.yaml b/deploy/charts/cert-manager/templates/webhook-validating-webhook.yaml index 780aa1ce4..de83b00ca 100644 --- a/deploy/charts/cert-manager/templates/webhook-validating-webhook.yaml +++ b/deploy/charts/cert-manager/templates/webhook-validating-webhook.yaml @@ -10,9 +10,7 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "webhook.chart" . }} annotations: -{{- if .Values.webhook.injectAPIServerCA }} - cert-manager.io/inject-apiserver-ca: "true" -{{- end }} + cert-manager.io/inject-ca-from-secret: "{{ .Release.Namespace }}/{{ include "webhook.servingCertificate" . }}" webhooks: - name: webhook.cert-manager.io namespaceSelector: @@ -42,7 +40,7 @@ webhooks: sideEffects: None clientConfig: service: - name: kubernetes - namespace: default - path: /apis/webhook.cert-manager.io/v1beta1/validations + name: {{ include "webhook.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + path: /mutate {{- end -}} diff --git a/go.mod b/go.mod index 43210b669..970303609 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,6 @@ require ( github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect - github.com/openshift/generic-admission-server v1.14.0 github.com/ory/dockertest v3.3.4+incompatible // indirect github.com/pascaldekloe/goe v0.1.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect @@ -114,8 +113,6 @@ require ( sigs.k8s.io/testing_frameworks v0.1.1 ) -replace github.com/openshift/generic-admission-server => github.com/openshift/generic-admission-server v1.14.0 - replace sigs.k8s.io/controller-runtime => github.com/munnerz/controller-runtime v0.1.8-0.20190907105316-d02b94982e57 replace github.com/evanphx/json-patch => github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550 diff --git a/go.sum b/go.sum index 543eed2c7..10d3204d7 100644 --- a/go.sum +++ b/go.sum @@ -400,8 +400,6 @@ github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVo github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/openshift/generic-admission-server v1.14.0 h1:GAQy5JNVcbmUuIpPvLd39+2rPecxEm7WQ2sP7ACrse4= -github.com/openshift/generic-admission-server v1.14.0/go.mod h1:GD9KN/W4KxqRQGVMbqQHpHzb2XcQVvLCaBaSciqXvfM= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/ory/dockertest v3.3.4+incompatible h1:VrpM6Gqg7CrPm3bL4Wm1skO+zFWLbh7/Xb5kGEbJRh8= github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= diff --git a/hack/build/repos.bzl b/hack/build/repos.bzl index e46808301..f934dcd64 100644 --- a/hack/build/repos.bzl +++ b/hack/build/repos.bzl @@ -1331,15 +1331,6 @@ def go_repositories(): sum = "h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=", version = "v0.1.1", ) - go_repository( - name = "com_github_openshift_generic_admission_server", - build_file_generation = "on", - build_file_proto_mode = "disable", - importpath = "github.com/openshift/generic-admission-server", - replace = "github.com/openshift/generic-admission-server", - sum = "h1:GAQy5JNVcbmUuIpPvLd39+2rPecxEm7WQ2sP7ACrse4=", - version = "v1.14.0", - ) go_repository( name = "com_github_ory_dockertest", build_file_generation = "on", diff --git a/pkg/webhook/BUILD.bazel b/pkg/webhook/BUILD.bazel index 3f0407523..5c6c4c69e 100644 --- a/pkg/webhook/BUILD.bazel +++ b/pkg/webhook/BUILD.bazel @@ -29,6 +29,7 @@ filegroup( srcs = [ ":package-srcs", "//pkg/webhook/handlers:all-srcs", + "//pkg/webhook/server:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/pkg/webhook/handlers/BUILD.bazel b/pkg/webhook/handlers/BUILD.bazel index 6c9b0caf4..b8353cc2c 100644 --- a/pkg/webhook/handlers/BUILD.bazel +++ b/pkg/webhook/handlers/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "interfaces.go", "mutation.go", "validation.go", ], diff --git a/pkg/webhook/handlers/interfaces.go b/pkg/webhook/handlers/interfaces.go new file mode 100644 index 000000000..5c3e42cab --- /dev/null +++ b/pkg/webhook/handlers/interfaces.go @@ -0,0 +1,54 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package handlers + +import ( + admissionv1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" + restclient "k8s.io/client-go/rest" +) + +type AdmissionHook interface { + // Initialize is called as a post-start hook + Initialize(kubeClientConfig *restclient.Config, stopCh <-chan struct{}) error +} + +type ValidatingAdmissionHook interface { + AdmissionHook + + // ValidatingResource is the resource to use for hosting your admission webhook. If the hook implements + // MutatingAdmissionHook as well, the two resources for validating and mutating admission must be different. + // Note: this is (usually) not the same as the payload resource! + ValidatingResource() (plural schema.GroupVersionResource, singular string) + + // Validate is called to decide whether to accept the admission request. The returned AdmissionResponse + // must not use the Patch field. + Validate(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse +} + +type MutatingAdmissionHook interface { + AdmissionHook + + // MutatingResource is the resource to use for hosting your admission webhook. If the hook implements + // ValidatingAdmissionHook as well, the two resources for validating and mutating admission must be different. + // Note: this is (usually) not the same as the payload resource! + MutatingResource() (plural schema.GroupVersionResource, singular string) + + // Admit is called to decide whether to accept the admission request. The returned AdmissionResponse may + // use the Patch field to mutate the object from the passed AdmissionRequest. + Admit(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse +} diff --git a/pkg/webhook/server/BUILD.bazel b/pkg/webhook/server/BUILD.bazel new file mode 100644 index 000000000..10a3dc348 --- /dev/null +++ b/pkg/webhook/server/BUILD.bazel @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "server.go", + "source.go", + "tls_file_source.go", + ], + importpath = "github.com/jetstack/cert-manager/pkg/webhook/server", + visibility = ["//visibility:public"], + deps = [ + "//pkg/logs:go_default_library", + "//pkg/webhook/handlers:go_default_library", + "@com_github_go_logr_logr//:go_default_library", + "@io_k8s_api//admission/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/runtime/schema:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/serializer/json:go_default_library", + "@io_k8s_sigs_controller_runtime//pkg/log: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/webhook/server/server.go b/pkg/webhook/server/server.go new file mode 100644 index 000000000..627a41f05 --- /dev/null +++ b/pkg/webhook/server/server.go @@ -0,0 +1,295 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "context" + "crypto/tls" + "fmt" + "io/ioutil" + "net" + "net/http" + "time" + + "github.com/go-logr/logr" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + crlog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/jetstack/cert-manager/pkg/webhook/handlers" +) + +var ( + defaultScheme = runtime.NewScheme() +) + +func init() { + admissionv1beta1.AddToScheme(defaultScheme) + + // we need to add the options to empty v1 + // TODO fix the server code to avoid this + metav1.AddToGroupVersion(defaultScheme, schema.GroupVersion{Version: "v1"}) + + // TODO: keep the generic API server from wanting this + unversioned := schema.GroupVersion{Group: "", Version: "v1"} + defaultScheme.AddUnversionedTypes(unversioned, + &metav1.Status{}, + &metav1.APIVersions{}, + &metav1.APIGroupList{}, + &metav1.APIGroup{}, + &metav1.APIResourceList{}, + ) +} + +type Server struct { + // ListenAddr is the address the HTTP server should listen on + // This must be specified. + ListenAddr string + + // HealthzAddr is the address the healthz HTTP server should listen on + // If not specified, the healthz endpoint will not be exposed. + HealthzAddr string + + // Scheme is used to decode/encode request/response payloads. + // If not specified, a default scheme that registers the AdmissionReview + // resource type will be used. + Scheme *runtime.Scheme + + // If specified, the server will listen with TLS using certificates + // provided by this CertificateSource. + CertificateSource CertificateSource + + ValidationWebhook handlers.ValidatingAdmissionHook + MutationWebhook handlers.MutatingAdmissionHook + + // Log is an optional logger to write informational and error messages to. + // If not specified, no messages will be logged. + Log logr.Logger +} + +func (s *Server) Run(stopCh <-chan struct{}) error { + if s.Log == nil { + s.Log = crlog.NullLogger{} + } + + internalStopCh := make(chan struct{}) + // only close the internalStopCh if it hasn't already been closed + shutdown := false + defer func() { + if !shutdown { + close(internalStopCh) + } + }() + + var healthzChan <-chan error + var certSourceChan <-chan error + + // if a HealthzAddr is provided, start the healthz listener + if s.HealthzAddr != "" { + l, err := net.Listen("tcp", s.HealthzAddr) + if err != nil { + return err + } + + mux := http.NewServeMux() + mux.HandleFunc("/healthz", s.handleHealthz) + mux.HandleFunc("/livez", s.handleLivez) + s.Log.Info("listening for insecure healthz connections", "address", s.HealthzAddr) + healthzChan = s.startServer(l, internalStopCh, mux) + } + + // create a listener for actual webhook requests + l, err := net.Listen("tcp", s.ListenAddr) + if err != nil { + return err + } + + // wrap the listener with TLS if a CertificateSource is provided + if s.CertificateSource != nil { + s.Log.Info("listening for secure connections", "address", s.ListenAddr) + certSourceChan = s.startCertificateSource(internalStopCh) + l = tls.NewListener(l, &tls.Config{ + GetCertificate: s.CertificateSource.GetCertificate, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + }) + } else { + s.Log.Info("listening for insecure connections", "address", s.ListenAddr) + } + + mux := http.NewServeMux() + mux.HandleFunc("/validate", s.handle(s.validate)) + mux.HandleFunc("/mutate", s.handle(s.mutate)) + listenerChan := s.startServer(l, internalStopCh, mux) + + if certSourceChan == nil { + certSourceChan = blockingChan(internalStopCh) + } + if healthzChan == nil { + healthzChan = blockingChan(internalStopCh) + } + + select { + case err = <-healthzChan: + case err = <-certSourceChan: + case err = <-listenerChan: + case <-stopCh: + } + + close(internalStopCh) + shutdown = true + + s.Log.Info("waiting for server to shutdown") + waitForAll(healthzChan, certSourceChan, listenerChan) + s.Log.Info("server shutdown successfully") + + return err +} + +func (s *Server) startServer(l net.Listener, stopCh <-chan struct{}, handle http.Handler) <-chan error { + ch := make(chan error) + go func() { + defer close(ch) + + srv := &http.Server{ + Handler: handle, + } + select { + case err := <-channelWrapper(func() error { return srv.Serve(l) }): + ch <- err + case <-stopCh: + // allow a fixed 5s for graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + s.Log.Error(err, "failed to gracefully shutdown http server") + ch <- err + } + s.Log.Info("shutdown HTTP server gracefully") + } + }() + return ch +} + +func (s *Server) startCertificateSource(stopCh <-chan struct{}) <-chan error { + fn := func() error { + return s.CertificateSource.Run(stopCh) + } + return channelWrapper(fn) +} + +func waitForAll(chs ...<-chan error) error { + for _, ch := range chs { + if err := <-ch; err != nil { + return fmt.Errorf("error waiting for goroutine to exit: %w", err) + } + } + return nil +} + +func channelWrapper(fn func() error) <-chan error { + ch := make(chan error) + go func() { + defer close(ch) + ch <- fn() + }() + return ch +} + +// blockingChan returns a 'no-op' error channel. +// When stopCh is closed, the error channel will also be closed. +func blockingChan(stopCh <-chan struct{}) <-chan error { + ch := make(chan error) + go func() { + defer close(ch) + <-stopCh + }() + return ch +} + +func (s *Server) scheme() *runtime.Scheme { + if s.Scheme == nil { + return defaultScheme + } + return s.Scheme +} + +func (s *Server) validate(obj runtime.Object) runtime.Object { + review := obj.(*admissionv1beta1.AdmissionReview) + resp := s.ValidationWebhook.Validate(review.Request) + review.Response = resp + return review +} + +func (s *Server) mutate(obj runtime.Object) runtime.Object { + review := obj.(*admissionv1beta1.AdmissionReview) + resp := s.MutationWebhook.Admit(review.Request) + review.Response = resp + return review +} + +func (s *Server) handle(inner func(runtime.Object) runtime.Object) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + data, err := ioutil.ReadAll(req.Body) + if err != nil { + s.Log.Error(err, "failed to read request body") + w.WriteHeader(http.StatusBadRequest) + return + } + + codec := json.NewSerializerWithOptions(json.DefaultMetaFactory, s.scheme(), s.scheme(), json.SerializerOptions{ + Pretty: true, + }) + codec.Decode(data, nil, nil) + obj, _, err := codec.Decode(data, nil, nil) + if err != nil { + s.Log.Error(err, "failed to decode request body") + w.WriteHeader(http.StatusBadRequest) + return + } + + result := inner(obj) + if err := codec.Encode(result, w); err != nil { + s.Log.Error(err, "failed to encode response body") + w.WriteHeader(http.StatusInternalServerError) + return + } + } +} + +func (s *Server) handleHealthz(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + if s.CertificateSource != nil && !s.CertificateSource.Healthy() { + s.Log.Info("Health check failed as CertificateSource is unhealthy") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (s *Server) handleLivez(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/webhook/server/source.go b/pkg/webhook/server/source.go new file mode 100644 index 000000000..c032def6a --- /dev/null +++ b/pkg/webhook/server/source.go @@ -0,0 +1,41 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import "crypto/tls" + +type CertificateSource interface { + // GetCertificate returns a Certificate based on the given + // ClientHelloInfo. It will only be called if the client supplies SNI + // information or if Certificates is empty. + // + // If GetCertificate is nil or returns nil, then the certificate is + // retrieved from NameToCertificate. If NameToCertificate is nil, the + // first element of Certificates will be used. + GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) + + // Run will start the certificate source. + // This may include setting up watches on certificate stores, or any other + // kind of background operation. + // The Run function should return when stopCh is closed, and may return an + // error if an irrecoverable error occurs whilst running. + Run(stopCh <-chan struct{}) error + + // Healthy can be used to check the status of the CertificateSource. + // It will return true if the source has a certificate available. + Healthy() bool +} diff --git a/pkg/webhook/server/tls_file_source.go b/pkg/webhook/server/tls_file_source.go new file mode 100644 index 000000000..402a4affb --- /dev/null +++ b/pkg/webhook/server/tls_file_source.go @@ -0,0 +1,154 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "bytes" + "crypto/tls" + "fmt" + "io/ioutil" + "sync" + "time" + + "github.com/go-logr/logr" + crlog "sigs.k8s.io/controller-runtime/pkg/log" + + logf "github.com/jetstack/cert-manager/pkg/logs" +) + +// FileCertificateSource provides certificate data for a golang HTTP server by +// reloading data on disk periodically. +type FileCertificateSource struct { + // CertPath is the path to the TLS certificate. + // This file will be read periodically and will be used as the private key + // for TLS connections. + CertPath string + + // KeyPath is the path to the private key. + // This file will be read periodically and will be used as the private key + // for TLS connections. + KeyPath string + + // UpdateInterval is how often the CertPath and KeyPath will be checked for + // changes. + // If not specified, a default of 10s will be used. + UpdateInterval time.Duration + + // MaxFailures is the maximum number of times a failure to read data from + // disk should be allowed before treating it as fatal. + // If not specified, a default of 12 will be used. + MaxFailures int + + // Log is an optional logger to write informational and error messages to. + // If not specified, no messages will be logged. + Log logr.Logger + + cachedCertificate *tls.Certificate + cachedCertBytes []byte + cachedKeyBytes []byte + lock sync.Mutex +} + +const defaultUpdateInterval = time.Second * 10 +const defaultMaxFailures = 12 + +var _ CertificateSource = &FileCertificateSource{} + +func (f *FileCertificateSource) Run(stopCh <-chan struct{}) error { + if f.Log == nil { + f.Log = crlog.NullLogger{} + } + + updateInterval := f.UpdateInterval + if updateInterval == 0 { + updateInterval = defaultUpdateInterval + } + maxFailures := f.MaxFailures + if maxFailures == 0 { + maxFailures = defaultMaxFailures + } + + // read the certificate data for the first time immediately, but allow + // retrying if the first attempt fails + if err := f.updateCertificateFromDisk(); err != nil { + f.Log.Error(err, "failed to read certificate from disk") + } + + failures := 0 + for { + select { + case <-stopCh: + return nil + case <-time.Tick(updateInterval): + if err := f.updateCertificateFromDisk(); err != nil { + failures++ + f.Log.Error(err, "failed to update certificate from disk", "failures", failures) + if failures >= maxFailures { + return fmt.Errorf("failed to update certificate from disk %d times: %v", failures, err) + } + continue + } + f.Log.V(logf.DebugLevel).Info("refreshed certificate from data on disk") + } + } +} + +func (f *FileCertificateSource) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { + f.lock.Lock() + defer f.lock.Unlock() + if f.cachedCertificate == nil { + return nil, fmt.Errorf("no tls.Certificate available") + } + return f.cachedCertificate, nil +} + +func (f *FileCertificateSource) Healthy() bool { + return f.cachedCertificate != nil +} + +// updateCertificateFromDisk will read private key and certificate data from +// disk and update the cached tls.Certificate if the data on disk has changed. +func (f *FileCertificateSource) updateCertificateFromDisk() error { + keyData, err := ioutil.ReadFile(f.KeyPath) + if err != nil { + return fmt.Errorf("failed to read keyPath: %w", err) + } + + certData, err := ioutil.ReadFile(f.CertPath) + if err != nil { + return fmt.Errorf("failed to read certPath: %w", err) + } + + f.lock.Lock() + defer f.lock.Unlock() + if bytes.Compare(keyData, f.cachedKeyBytes) == 0 && bytes.Compare(certData, f.cachedCertBytes) == 0 { + f.Log.V(logf.DebugLevel).Info("key and certificate on disk have not changed") + return nil + } + f.Log.Info("detected private key or certificate data on disk has changed. reloading certificate") + + cert, err := tls.X509KeyPair(certData, keyData) + if err != nil { + return err + } + + f.cachedCertBytes = certData + f.cachedKeyBytes = keyData + f.cachedCertificate = &cert + + return nil +}