From 3b55f8652c984da3336727264d79197bc5ccfe27 Mon Sep 17 00:00:00 2001 From: Jing Xu Date: Thu, 27 Sep 2018 15:23:39 -0700 Subject: [PATCH] Add admission webhook This PR adds the admission webhook for verifying snapshot API objects and also seting the default snapshot class if it is not specified by the users --- cmd/csi-snapshotter/main.go | 29 +++ hack/gencerts.sh | 78 ++++++++ pkg/webhook/certs.go | 112 ++++++++++++ pkg/webhook/webhook.go | 343 ++++++++++++++++++++++++++++++++++++ 4 files changed, 562 insertions(+) create mode 100755 hack/gencerts.sh create mode 100644 pkg/webhook/certs.go create mode 100644 pkg/webhook/webhook.go diff --git a/cmd/csi-snapshotter/main.go b/cmd/csi-snapshotter/main.go index 80e1ff077..4e99eb9a2 100644 --- a/cmd/csi-snapshotter/main.go +++ b/cmd/csi-snapshotter/main.go @@ -23,8 +23,10 @@ import ( "os" "os/signal" "time" + "net/http" "github.com/golang/glog" + "github.com/pkg/errors" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -36,6 +38,7 @@ import ( clientset "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned" snapshotscheme "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned/scheme" informers "github.com/kubernetes-csi/external-snapshotter/pkg/client/informers/externalversions" + "github.com/kubernetes-csi/external-snapshotter/pkg/webhook" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" ) @@ -145,6 +148,11 @@ func main() { os.Exit(1) } + if err := webhook.CreateConfiguration(kubeClient, "webhooksURL"); err != nil { + glog.Error("Failed to create configuration for Webhook, %v", err) + os.Exit(1) + } + glog.V(2).Infof("Start NewCSISnapshotController with snapshotter [%s] kubeconfig [%s] connectionTimeout [%+v] csiAddress [%s] createSnapshotContentRetryCount [%d] createSnapshotContentInterval [%+v] resyncPeriod [%+v] snapshotNamePrefix [%s] snapshotNameUUIDLength [%d]", *snapshotter, *kubeconfig, *connectionTimeout, *csiAddress, createSnapshotContentRetryCount, *createSnapshotContentInterval, *resyncPeriod, *snapshotNamePrefix, snapshotNameUUIDLength) ctrl := controller.NewCSISnapshotController( @@ -166,6 +174,7 @@ func main() { // run... stopCh := make(chan struct{}) factory.Start(stopCh) + go startWebhooksHTTPs(kubeClient, snapClient) go ctrl.Run(threads, stopCh) // ...until SIGINT @@ -203,3 +212,23 @@ func waitForDriverReady(csiConn connection.CSIConnection, timeout time.Duration) time.Sleep(time.Second) } } + +func startWebhooksHTTPs(kubeClientset kubernetes.Interface, snapClient clientset.Interface) error { + glog.V(2).Infof("Starting Snapshot webhooks HTTPS handler") + defer glog.V(2).Infof("Stopping Snapshot webhooks HTTPS handler") + + mux := http.NewServeMux() + mux.Handle("/", http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + webhook.AdmitSnapshot(w, r, kubeClientset, snapClient) + })) + server := &http.Server{ + Addr: ":443", + TLSConfig: webhook.GetTLSConfig(kubeClientset), + Handler: mux, + } + if err := server.ListenAndServeTLS("", ""); err != http.ErrServerClosed { + return errors.Wrap(err, "start webhook HTTPS handler") + } + return nil +} diff --git a/hack/gencerts.sh b/hack/gencerts.sh new file mode 100755 index 000000000..5629f898c --- /dev/null +++ b/hack/gencerts.sh @@ -0,0 +1,78 @@ + +#!/bin/bash + +# Copyright 2018 The Kubernetes Authors. All rights reserved. +# +# 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. + + +# Generates the a CA cert, a server key, and a server cert signed by the CA. +# reference: https://github.com/kubernetes/kubernetes/blob/master/plugin/pkg/admission/webhook/gencerts.sh +set -e + +CN_BASE="snapshot" +CN="snapshot-admission-webhook-svc.default.svc" + +cat > server.conf << EOF +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name +[req_distinguished_name] +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth, serverAuth +EOF + +# Create a certificate authority +openssl genrsa -out caKey.pem 2048 +openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca" + +# Create a server certificate +openssl genrsa -out serverKey.pem 2048 +# Note the CN is the DNS name of the service of the webhook. +openssl req -new -key serverKey.pem -out server.csr -subj "/CN=${CN}" -config server.conf +openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf + +outfile=pkg/webhook/certs.go + +cat > $outfile << EOF +// Copyright 2018 Google Inc. All Rights Reserved. +// +// 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. +EOF + +echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile +echo "" >> $outfile +echo "package webhook" >> $outfile +for file in caKey caCert serverKey serverCert; do + data=$(cat ${file}.pem) + echo "" >> $outfile + echo "var $file = []byte(\`$data\`)" >> $outfile +done + +# Clean up after we're done. +rm *.pem +rm *.csr +rm *.srl +rm *.conf \ No newline at end of file diff --git a/pkg/webhook/certs.go b/pkg/webhook/certs.go new file mode 100644 index 000000000..94be37a80 --- /dev/null +++ b/pkg/webhook/certs.go @@ -0,0 +1,112 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// 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. +// This file was generated using openssl by the gencerts.sh script + +package webhook + +var caKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuVbxVMXgIyoEPRj6XdX/uCGcz7SSrzDYsgFvWFuEtEiFWXZ7 +izGhM+KtgrFxwchr/J+ZIFO6tOjX+uM1LuQzjLn3MfFoJtdGANAm8Kqus8eOOXOb +78/JELdGYkkrYENWIsSvcB81y3ahjgAPV4X74ZjPVuGjd2EVRgFtOfYOaGYzehdh +Ih47CxjMKYPZ5T3F6MptY1YYxEBfCGl7PuEkRUd8jnqubYCbnN8Tch1LOVrNPgc2 +M5KY4/P1xiftFJU4KhWvqceicYc2/0le3Hu29m5L2lFDK2OF2gcIpbmF1LPeuzol +u7XzRqteSU9/BOGNGgHF0808jgIMU39DQGyuXwIDAQABAoIBAFVFMvMVtvF2u0yA +2W+irWxByqulIHeJuajsEBZOxNdNJkzqvhxkUQ5WFA41JWlKlKQ9qW2+GABzwCql +ripMw4rTZG+N6aU1Fff5zHCdlpMQFNdJ4UyMBK20JKXDlDlNwattYmnrcgySH/H9 +BRH3itNYQsxuM48RE4CJ1377PdW5pplj8urMJ6vaQ5FdBnxFpThMlyuqSezk/GqA +iXAB0VHt2JvkSe8XXIzVtkKR6FnfaGT5dH020/Ia4w07zY4l6rBvSN2iaW8a9trc +MsZug9At+I+/WVBDS97PRrmEREECPJJB78E9F+nPxEToYTns5zrKz8m3N3LX0CcK +ZQV6w8ECgYEA64rdB/5wwBxjHY+YqmaxezQXS1TT8Z3hkHw10BPeabYSzmiCDzwh +2lQztiA2mBhTsyOG82uz/BFQEEB+6d6cf77BJuh4pk7w4Mg+UYoHoBfIr7zC2rfS +RUC1uRSvgYWTf/MPKPKzSJpEVaVV8IBn+ibpwKFURMFhIdL/C+Qe1ZsCgYEAyW/Z +/1s/BGhXrqNsPVbqAyIKJXNcaJDzNNfktY43w9U9sUIMrW2J2trjK6qJ5Itlfro9 +NXquchd0pYm+ysAqF8vmN4vB/FN0QcnvBSYoM2iDtc8+C/G6cjt9adiJQTDYh1yF +/I3xz+d/bynyTgoYKc7fsMyHflYcK4N3wmYnmI0CgYEArTYG6NQBkiTN9nUcrWKr +bZCW+Ly+x1V1BM1yvTt/OXm9RrCvxAhSVL3K8UmrHBn3oyqjGOrBBsKsf+cN7WnY +6FActkIKRzKSDJr0yP2aMe6LlEBZgoHfTTIS8LH3hmX2XAcfxNsFYIShb+IP2rZy +wBBRoWiCEbWrejYxfEsbKbUCgYB8HEFJlzO1iIB1plUbWgCm24M63eASwTRH27kb +r7tmGm1/WH2tIS9tu616CwIY4VYwhZkO6T6wJwmEsODv1QRaUxPOJ3rm95hKrJtr +Jb5hJkT4cO7+tvo0RbkYzQSMOQdAJ16aY+6YNT8MA+E5+fg3UjH6oZnd2jpTCRZx +nTVKRQKBgEXRMBHJv6xzF0vfdsNTftgX5vfOyHRFuGzL04a0evv6Zj8izPJ2rOwy +tbFfCvmMiNqBANTR9YxM9fDv2Aq7qWUSJ2IqVS6nXw0EDj3TXH+Pur2LfQPACKtD +1Oe+4t0kmon2We6o9hNM9nR3EjFA0zDauKwRBSrjrlGaf4tTsc/6 +-----END RSA PRIVATE KEY-----`) + +var caCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDBDCCAeygAwIBAgIJAPLPw2bFlYqUMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV +BAMMC3NuYXBzaG90X2NhMCAXDTE4MDkyNzIwNTIzNloYDzIyOTIwNzEyMjA1MjM2 +WjAWMRQwEgYDVQQDDAtzbmFwc2hvdF9jYTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALlW8VTF4CMqBD0Y+l3V/7ghnM+0kq8w2LIBb1hbhLRIhVl2e4sx +oTPirYKxccHIa/yfmSBTurTo1/rjNS7kM4y59zHxaCbXRgDQJvCqrrPHjjlzm+/P +yRC3RmJJK2BDViLEr3AfNct2oY4AD1eF++GYz1bho3dhFUYBbTn2DmhmM3oXYSIe +OwsYzCmD2eU9xejKbWNWGMRAXwhpez7hJEVHfI56rm2Am5zfE3IdSzlazT4HNjOS +mOPz9cYn7RSVOCoVr6nHonGHNv9JXtx7tvZuS9pRQytjhdoHCKW5hdSz3rs6Jbu1 +80arXklPfwThjRoBxdPNPI4CDFN/Q0Bsrl8CAwEAAaNTMFEwHQYDVR0OBBYEFFGw +D7vzl/nFIQ8h5rcqc6LsOVY5MB8GA1UdIwQYMBaAFFGwD7vzl/nFIQ8h5rcqc6Ls +OVY5MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEVO/V0PFB9E +wujSbOYCIbx+51yDUoC8hxT1r1Kcx3u08MDgUoDX+Rxg/Ok7XbqweCvR0drbSKAA +kLE96CVL98gonTK8xq/wr6ajEu4Zkq2gG+uTDnk09Rw3tQDlu5a8y2xx3tkPwXZQ +mJlOKfRQJT16scQYJI2vYlfD02sIAz82+Y6tJTe8X4HviSPx4l65V1gP1Cv0IRQN +FmZf7z8vxhvuO2bc4kEfD2ROtn8JR6IohFkqbsgLblZleFlsjnoS+wZr/hg4OGKN +Ra00JO66qaNYwwueeKU4Qvfo/HiWXwRivsCmNkkuzneovYI9WAnspQXg6szMMKxj +g1O6iPylFcU= +-----END CERTIFICATE-----`) + +var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA2VeAQeGNCBfuCQaqQIEspJFQNnal47TytS5LDgH5RccoRkIs +kCsecdPEZeLqHnuDEMg9M6DPWjl4VHkPnBCUkCVVGjvhb1c4A5LI++zKIZkhvUvY +zpsS6emQs+Ji+86I83EgxpjBXzEgD0NffOfaDbLdLFA+B0R7jOyaIquvvMPXcca7 +z1EDbN0zuUPmPHBa9LRLs4TlEbn8gGpL+ajSyBqrsL5qP92VUq8wcFpUXJ50xocD +B5tZc8ESmIiYjIVXaFvIZu1RAIG2Sj68HOpbU61T/Ftdc3y4kB8+9rTUFaTvSVvD +ShW3sUqFULD6qv+vg+GzRAWGM+sUjhvx0VZrzQIDAQABAoIBADNG56Wjd0ifjLNo +dSZ+02+IMSqIV58fg9unLmIBqTQDRsc8uZLR+X7VfZKkNKQ4c7Wz6GyT1hftUyxq +23nwl3xism9cDVXdeAOvz+dP+Ghw3nrwuBgWkiHJzzABi2TpV3pICHmSdJzm1C+F +r7OiZ9mvh2r7C4dfat7Wu47OfsnD42ziS9p3R6hcvugPoaAy23IwAgmR8LwoOXGy +ZiAkb/d30G0MNt3lFbeZKHL16ibRxPjCRnDrfI++4/iJhuxBVv0BebYEjvajAFby +G/bQNfXSghpYAywnCMoTqUVCV8ZALbw+okGrcxThcI7ZKghVGcYeK9/n5+tg7+r9 +I1Q3J2kCgYEA8ZW3xhRr3XwuJCUFBs5yB0ibjV31w5slvSBrhd78gGLij8DRRynt +U/rro0Vk0rw3VzKJvk9rohR0YbQBDIrtfVNWzkJOZJXDDwmb6b8ooUcADl5ui9Ul +NvgwzQtq/p/nDfPNbwzMN4XiwfnQ/+yq0J98+RvEM6bDlYOT6wma8hsCgYEA5k92 +oDfyIGqI3Cx1jFnRwxTo5A6yW73B0XkIFOe8P2PMhntjt7rpE3gq3VVfDbemVSI5 +WXY5wh0daOFkWzNbuvudHxslWQrZSyG71ycEcAHfxWuwqOLiGn4x3/CXzIJsyR8L +jkhApZ2J6YwotxFk9kh/DW+vOpbPKUdPZbGruDcCgYEA3f4T90LAs6/uvmv+KHkA +M003EzpqIaqpjRcDduqm4Fr9kdc+98PBP9BtQ4T61uL5f3kDNgvI/hEJuNYtuJbZ +ELbKJ5KqcqdjrKfJy4tLDJgvpwSDVJ8yKUb7oQ+C7COHsDx+ZDNAXSz8Z/7lXKbf +eAF2V3p6WnQ9eWCFRg93gE0CgYBNGR7aBcB9T4yfQBbdtBe/WZmY9r6IbZ6bdAvb +i7P9+He4MUgxclWiGeEnlPOsEOWSrFFMfIJbVAnLWWCSE0BK+P4hMqIvC62wNAvA +u6QFpur1GNbbwo/0VHh3wf/fC25FaaohqFhT2MgZMb1Tg3Qr6hr2MYQUdfXFmMSg +g3i7wwKBgQCPv2YxHynKq/N6Ns8OUAAGatIc8BG07EBT79T8UPiGi7Ova3jIoQLp +UkiSKdtPaYDnjUG/a4MkVbNATNh48rveW/U5BumLMMFQtHK44vOQFX9fGS/zLKkZ +yt8uo1eX6Uo3QGuXaLFKgu0sXRRzBtZBu/6AUzTNA5gKz9qIBKkfeA== +-----END RSA PRIVATE KEY-----`) + +var serverCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIJAMT8cU6Xrf9/MA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV +BAMMC3NuYXBzaG90X2NhMCAXDTE4MDkyNzIwNTIzN1oYDzIyOTIwNzEyMjA1MjM3 +WjA1MTMwMQYDVQQDDCpzbmFwc2hvdC1hZG1pc3Npb24td2ViaG9vay1zdmMuZGVm +YXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZV4BB4Y0I +F+4JBqpAgSykkVA2dqXjtPK1LksOAflFxyhGQiyQKx5x08Rl4uoee4MQyD0zoM9a +OXhUeQ+cEJSQJVUaO+FvVzgDksj77MohmSG9S9jOmxLp6ZCz4mL7zojzcSDGmMFf +MSAPQ19859oNst0sUD4HRHuM7Joiq6+8w9dxxrvPUQNs3TO5Q+Y8cFr0tEuzhOUR +ufyAakv5qNLIGquwvmo/3ZVSrzBwWlRcnnTGhwMHm1lzwRKYiJiMhVdoW8hm7VEA +gbZKPrwc6ltTrVP8W11zfLiQHz72tNQVpO9JW8NKFbexSoVQsPqq/6+D4bNEBYYz +6xSOG/HRVmvNAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1Ud +JQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEADQw8 +fw/RgOTtx9QVxUYMVD55VtdX/xctaAllTnE3A2T03/o73pdLs8owaPd0CbUFZO4m +0AgaSzOamd3HHTY9TBxtddGh1wU+yxWMB+XhffAQwHpxIZ/BVMhu3pf5024D7S6k +5dskfY/011+JmCi28Rfwb86mlwluR4fOczSTW22815aZv8LXxsCrwzbft86pa+dQ +b6lzWsT6H+BE92xBCpgrZyMBCSs0hXfDQV7f0lVJcD+Z3bJkDPO/8yue2IyluUx+ +Eovpt5wY4AhiorLD5xw5jY4cFLooDLOyV8VGJ56GuAUVpvYKivD92k5whTl7fSAR +xsubPWWjGPtNl9vpyQ== +-----END CERTIFICATE-----`) diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go new file mode 100644 index 000000000..7ea88a338 --- /dev/null +++ b/pkg/webhook/webhook.go @@ -0,0 +1,343 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// 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 webhook + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + + "github.com/mattbaird/jsonpatch" + //"github.com/pkg/errors" + //"github.com/sirupsen/logrus" + "github.com/golang/glog" + + admv1beta1 "k8s.io/api/admission/v1beta1" + admregv1beta1 "k8s.io/api/admissionregistration/v1beta1" + "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + kubeclientset "k8s.io/client-go/kubernetes" + + snapshotv1alpha1 "github.com/kubernetes-csi/external-snapshotter/pkg/apis/volumesnapshot/v1alpha1" + clientset "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned" +) + +const ( + webhookConfigName = "snapshot-webhook-config" + snapshotWebhookName = "snapshot-init.snapshot.storage.k8s.io" + // The admission webhook must be exposed in the following service, + // this is mainly for the server certificate. + // serviceName is the name of the admission webhook service. + serviceName = "snapshot-admission-webhook-svc" + + IsDefaultSnapshotClassAnnotation = "snapshot.storage.kubernetes.io/is-default-class" +) + +var supportedSourceKinds = sets.NewString(string("PersistentVolumeClaim")) +var supportedDataSourceAPIGroups = sets.NewString(string("")) + +// CreateConfiguration creates MutatingWebhookConfiguration and registeres the +// webhook admission controller with the kube-apiserver +func CreateConfiguration(clientset kubeclientset.Interface, url string) error { + failurePolicy := admregv1beta1.Fail + config := &admregv1beta1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: webhookConfigName, + }, + Webhooks: []admregv1beta1.Webhook{ + // Webhook for initializing Snapshots. + { + Name: snapshotWebhookName, + ClientConfig: admregv1beta1.WebhookClientConfig{ + Service: &admregv1beta1.ServiceReference{ + Name: serviceName, + Namespace: "default", + }, + CABundle: caCert, + }, + Rules: []admregv1beta1.RuleWithOperations{ + { + Operations: []admregv1beta1.OperationType{ + admregv1beta1.Create, + }, + Rule: admregv1beta1.Rule{ + APIGroups: []string{snapshotv1alpha1.GroupName}, + APIVersions: []string{"v1alpha1"}, + Resources: []string{"volumesnapshots"}, + }, + }, + }, + FailurePolicy: &failurePolicy, + }, + }, + } + glog.V(4).Infof("Creating MutatingWebhookConfigurations %q", config.Name) + if _, err := clientset.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(config); err != nil { + if apierrors.IsAlreadyExists(err) { + glog.V(2).Infof("MutatingWebhookConfigurations %q already exists; use the existing one", config.Name) + return nil + } + return fmt.Errorf("failed to create MutatingWebhookConfigurations %q: %v", config.Name, err) + } + return nil +} + +func GetTLSConfig(clientset kubeclientset.Interface) *tls.Config { + sCert, err := tls.X509KeyPair(serverCert, serverKey) + if err != nil { + glog.Fatal(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{sCert}, + } +} + +// AdmitSnapshot performs admission checks and mutations on VolumeSnapshots. +func AdmitSnapshot(writer http.ResponseWriter, req *http.Request, clientset kubeclientset.Interface, snapClient clientset.Interface) { + glog.Infof("admit snapshot ") + review := &admv1beta1.AdmissionReview{} + if err := json.NewDecoder(req.Body).Decode(review); err != nil { + glog.Infof("Failed to decode Admit request: req = %+v, err = %s", *req, err) + writer.WriteHeader(http.StatusBadRequest) + return + } + response := admitSnapshot(review, clientset, snapClient) + glog.V(5).Infof("Processed admission review: %+v", review) + sendResponse(writer, response) +} + +func admitSnapshot(review *admv1beta1.AdmissionReview, clientset kubeclientset.Interface, snapClient clientset.Interface) *admv1beta1.AdmissionReview { + review.Response = &admv1beta1.AdmissionResponse{} + + // Verify that the request is indeed a Snapshot object. + resource := metav1.GroupVersionResource{ + Group: snapshotv1alpha1.GroupName, + Version: "v1alpha1", + Resource: "volumesnapshots", + } + if review.Request.Resource != resource { + review.Response.Result = &metav1.Status{ + Reason: metav1.StatusReasonInvalid, + Message: fmt.Sprintf("unexpected resource %+v in VolumeSnapshot admission", review.Request.Resource), + } + return review + } + + // Decode the request + snapshot := &snapshotv1alpha1.VolumeSnapshot{} + if err := json.Unmarshal(review.Request.Object.Raw, snapshot); err != nil { + review.Response.Result = &metav1.Status{ + Reason: metav1.StatusReasonInvalid, + Message: fmt.Sprintf("failed to decode VolumeSnapshot object %s/%s", review.Request.Namespace, review.Request.Name), + } + return review + } + + // Validate the Snapshot object and set default snapshot class if not specified + newSnapshot, changed, err := validateSnapshot(snapshot, clientset, snapClient) + if err != nil { + review.Response.Result = &metav1.Status{ + Reason: metav1.StatusReasonInvalid, + Message: err.Error(), + } + return review + } + + if changed { + glog.V(5).Infof("Patched Snapshot %s/%s: +%v", snapshot.Namespace, snapshot.Name, newSnapshot) + patch, err := createPatch(review.Request.Object.Raw, newSnapshot) + if err != nil { + review.Response.Result = &metav1.Status{ + Reason: metav1.StatusReasonInternalError, + Message: fmt.Sprintf("failed to create patch for snapshot %s/%s", snapshot.Namespace, snapshot.Name), + } + return review + } + patchType := admv1beta1.PatchTypeJSONPatch + review.Response.Patch = patch + review.Response.PatchType = &patchType + } + + review.Response.Allowed = true + return review +} + +func createPatch(old []byte, newObj interface{}) ([]byte, error) { + new, err := json.Marshal(newObj) + if err != nil { + return nil, err + } + patch, err := jsonpatch.CreatePatch(old, new) + if err != nil { + return nil, err + } + return json.Marshal(patch) +} + +// IsDefaultAnnotation returns a boolean if +// the annotation is set +func IsDefaultAnnotation(obj metav1.ObjectMeta) bool { + if obj.Annotations[IsDefaultSnapshotClassAnnotation] == "true" { + return true + } + + return false +} + +func getDefaultSnapshotClassName(driver string, snapClient clientset.Interface) (string, error) { + list, err := snapClient.VolumesnapshotV1alpha1().VolumeSnapshotClasses().List(metav1.ListOptions{}) + if err != nil { + return "", err + } + defaultClasses := []snapshotv1alpha1.VolumeSnapshotClass{} + + for _, class := range list.Items { + if IsDefaultAnnotation(class.ObjectMeta) && class.Snapshotter == driver { + defaultClasses = append(defaultClasses, class) + glog.V(5).Infof("get defaultClass added: %s", class.Name) + } + } + if len(defaultClasses) == 0 { + return "", fmt.Errorf("cannot find default snapshot class") + } + if len(defaultClasses) > 1 { + glog.V(4).Infof("get DefaultClass %d defaults found", len(defaultClasses)) + return "", fmt.Errorf("%d default snapshot classes were found", len(defaultClasses)) + } + return defaultClasses[0].Name, nil +} + +func getSnapshotClassNameFromContent(content *snapshotv1alpha1.VolumeSnapshotContent, snapClient clientset.Interface) (string, error) { + if content.Spec.VolumeSnapshotClassName != nil { + return *content.Spec.VolumeSnapshotClassName, nil + } + if content.Spec.VolumeSnapshotSource.CSI == nil { + return "", fmt.Errorf("VolumeSNapshotContent does not has CSI volume source") + } + return getDefaultSnapshotClassName(content.Spec.VolumeSnapshotSource.CSI.Driver, snapClient) +} + +// validateSnapshot checks whether the values set on the VolumeSnapshot object are valid. +// also set the snapshot class if it is not specified +func validateSnapshot(snapshot *snapshotv1alpha1.VolumeSnapshot, clientset kubeclientset.Interface, snapClient clientset.Interface) (*snapshotv1alpha1.VolumeSnapshot, bool, error) { + if snapshot.Spec.Source != nil { + source := snapshot.Spec.Source + if len(source.Name) == 0 { + return nil, false, fmt.Errorf("Snapshot.Spec.Source.Name can not be empty") + } else if !supportedSourceKinds.Has(string(source.Kind)) { + return nil, false, fmt.Errorf("Snapshot.Spec.Source.Kind exepct %v, got %q", supportedSourceKinds, source.Kind) + } + pvc, err := validateVolume(snapshot, clientset) + if err != nil { + return nil, false, err + } + exist, driver, err := validateSnapshotClass(snapshot, pvc, clientset, snapClient) + if err != nil { + return nil, false, err + } + if exist { + return snapshot, false, nil + } + defaultClassName, err := getDefaultSnapshotClassName(driver, snapClient) + if err != nil { + return nil, false, err + } + snapshotClone := snapshot.DeepCopy() + snapshotClone.Spec.VolumeSnapshotClassName = &defaultClassName + return snapshotClone, true, nil + } + + // if source is not specified, the content name must be set + if snapshot.Spec.SnapshotContentName == "" { + return nil, false, fmt.Errorf("cannot set Snapshot.Spec.Source to nil and snapshot.Spec.SnapshotContentName to empty at the same time.") + } + if snapshot.Spec.VolumeSnapshotClassName == nil { + content, err := snapClient.VolumesnapshotV1alpha1().VolumeSnapshotContents().Get(snapshot.Spec.SnapshotContentName, metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + className, err := getSnapshotClassNameFromContent(content, snapClient) + if err != nil { + return nil, false, fmt.Errorf("cannot get default storage class %v", err) + } + snapshotClone := snapshot.DeepCopy() + snapshotClone.Spec.VolumeSnapshotClassName = &className + return snapshotClone, true, nil + } + return nil, false, nil +} + +func validateVolume(snapshot *snapshotv1alpha1.VolumeSnapshot, clientset kubeclientset.Interface) (*v1.PersistentVolumeClaim, error) { + pvcName := snapshot.Spec.Source.Name + if pvcName == "" { + return nil, fmt.Errorf("the PVC name is not specified in snapshot %s/%s", snapshot.Namespace, snapshot.Name) + } + pvc, err := clientset.CoreV1().PersistentVolumeClaims(snapshot.Namespace).Get(pvcName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to retrieve PVC %s from the API server: %q", pvcName, err) + } + if pvc.Status.Phase != v1.ClaimBound { + return nil, fmt.Errorf("the PVC %s is not yet bound to a PV, will not attempt to take a snapshot", pvc.Name) + } + pvName := pvc.Spec.VolumeName + _, err = clientset.CoreV1().PersistentVolumes().Get(pvName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to retrieve PV %s from the API server: %q", pvName, err) + } + + glog.V(5).Infof("getVolumeFromVolumeSnapshot: snapshot [%s] PV name [%s]", snapshot.Name, pvName) + + return pvc, nil +} + +func validateSnapshotClass(snapshot *snapshotv1alpha1.VolumeSnapshot, pvc *v1.PersistentVolumeClaim, clientset kubeclientset.Interface, snapClient clientset.Interface) (bool, string, error) { + storageClassName := pvc.Spec.StorageClassName + if storageClassName == nil || len(*storageClassName) == 0 { + return false, "", fmt.Errorf("fail to get storage class from the pvc source") + } + + storageClass, err := clientset.StorageV1().StorageClasses().Get(*storageClassName, metav1.GetOptions{}) + if err != nil { + return false, "", fmt.Errorf("failed to retrieve StorageClass %q from the API server: %v", storageClassName, err) + } + + snapshotClassName := snapshot.Spec.VolumeSnapshotClassName + if snapshotClassName == nil || len(*snapshotClassName) == 0 { + return false, storageClass.Provisioner, nil + } + snapshotClass, err := snapClient.VolumesnapshotV1alpha1().VolumeSnapshotClasses().Get(*snapshotClassName, metav1.GetOptions{}) + if err != nil { + return false, "", fmt.Errorf("failed to retrieve VolumeSnapshotClass %q from the API server: %v", snapshotClassName, err) + } + + if storageClass.Provisioner != snapshotClass.Snapshotter { + return true, storageClass.Provisioner, fmt.Errorf("the snapshotter driver specified in snapshot class does not match the driver from the volume's provisioner") + } + return true, storageClass.Provisioner, nil +} + +// sendResponse sends the response using the writer. +func sendResponse(writer http.ResponseWriter, response interface{}) { + b, err := json.Marshal(response) + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + return + } + writer.WriteHeader(http.StatusOK) + writer.Write(b) +}