From 711e3cff9812b5a1d19ffca2c9b3ff0ab6963491 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Thu, 18 Aug 2016 21:59:45 -0400 Subject: [PATCH] Add new admission controller: image policy webhook --- hack/.linted_packages | 3 +- plugin/pkg/admission/imagepolicy/admission.go | 239 +++++ .../admission/imagepolicy/admission_test.go | 944 ++++++++++++++++++ .../pkg/admission/imagepolicy/certs_test.go | 211 ++++ plugin/pkg/admission/imagepolicy/config.go | 93 ++ .../pkg/admission/imagepolicy/config_test.go | 105 ++ plugin/pkg/admission/imagepolicy/doc.go | 18 + plugin/pkg/admission/imagepolicy/gencerts.sh | 102 ++ 8 files changed, 1714 insertions(+), 1 deletion(-) create mode 100644 plugin/pkg/admission/imagepolicy/admission.go create mode 100644 plugin/pkg/admission/imagepolicy/admission_test.go create mode 100644 plugin/pkg/admission/imagepolicy/certs_test.go create mode 100644 plugin/pkg/admission/imagepolicy/config.go create mode 100644 plugin/pkg/admission/imagepolicy/config_test.go create mode 100644 plugin/pkg/admission/imagepolicy/doc.go create mode 100755 plugin/pkg/admission/imagepolicy/gencerts.sh diff --git a/hack/.linted_packages b/hack/.linted_packages index f0f6d0554c1..283832a2089 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -169,6 +169,7 @@ plugin/pkg/admission/admit plugin/pkg/admission/alwayspullimages plugin/pkg/admission/deny plugin/pkg/admission/exec +plugin/pkg/admission/imagepolicy plugin/pkg/admission/namespace/autoprovision plugin/pkg/admission/namespace/exists plugin/pkg/admission/securitycontext/scdeny @@ -202,4 +203,4 @@ pkg/util/maps pkg/volume/quobyte test/integration/discoverysummarizer test/integration/examples -test/integration/federation +test/integration/federation \ No newline at end of file diff --git a/plugin/pkg/admission/imagepolicy/admission.go b/plugin/pkg/admission/imagepolicy/admission.go new file mode 100644 index 00000000000..54d658c538c --- /dev/null +++ b/plugin/pkg/admission/imagepolicy/admission.go @@ -0,0 +1,239 @@ +/* +Copyright 2016 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. +*/ + +// Package imagepolicy contains an admission controller that configures a webhook to which policy +// decisions are delegated. +package imagepolicy + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "time" + + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/api" + apierrors "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/imagepolicy/v1alpha1" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/util/yaml" + + "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/util/cache" + "k8s.io/kubernetes/plugin/pkg/webhook" + + "k8s.io/kubernetes/pkg/admission" +) + +var ( + groupVersions = []unversioned.GroupVersion{v1alpha1.SchemeGroupVersion} +) + +func init() { + admission.RegisterPlugin("ImagePolicyWebhook", func(client clientset.Interface, config io.Reader) (admission.Interface, error) { + newImagePolicyWebhook, err := NewImagePolicyWebhook(client, config) + if err != nil { + return nil, err + } + return newImagePolicyWebhook, nil + }) +} + +// imagePolicyWebhook is an implementation of admission.Interface. +type imagePolicyWebhook struct { + *admission.Handler + webhook *webhook.GenericWebhook + responseCache *cache.LRUExpireCache + allowTTL time.Duration + denyTTL time.Duration + retryBackoff time.Duration + defaultAllow bool +} + +func (a *imagePolicyWebhook) statusTTL(status v1alpha1.ImageReviewStatus) time.Duration { + if status.Allowed { + return a.allowTTL + } + return a.denyTTL +} + +// Filter out annotations that don't match *.image-policy.k8s.io/* +func (a *imagePolicyWebhook) filterAnnotations(allAnnotations map[string]string) map[string]string { + annotations := make(map[string]string) + for k, v := range allAnnotations { + if strings.Contains(k, ".image-policy.k8s.io/") { + annotations[k] = v + } + } + return annotations +} + +// Function to call on webhook failure; behavior determined by defaultAllow flag +func (a *imagePolicyWebhook) webhookError(attributes admission.Attributes, err error) error { + if err != nil { + glog.V(2).Infof("error contacting webhook backend: %s") + if a.defaultAllow { + glog.V(2).Infof("resource allowed in spite of webhook backend failure") + return nil + } + glog.V(2).Infof("resource not allowed due to webhook backend failure ") + return admission.NewForbidden(attributes, err) + } + return nil +} + +func (a *imagePolicyWebhook) Admit(attributes admission.Attributes) (err error) { + // Ignore all calls to subresources or resources other than pods. + allowedResources := map[unversioned.GroupResource]bool{ + api.Resource("pods"): true, + } + + if len(attributes.GetSubresource()) != 0 || !allowedResources[attributes.GetResource().GroupResource()] { + return nil + } + + pod, ok := attributes.GetObject().(*api.Pod) + if !ok { + return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") + } + + // Build list of ImageReviewContainerSpec + var imageReviewContainerSpecs []v1alpha1.ImageReviewContainerSpec + containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)) + containers = append(containers, pod.Spec.Containers...) + containers = append(containers, pod.Spec.InitContainers...) + for _, c := range containers { + imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{ + Image: c.Image, + }) + } + imageReview := v1alpha1.ImageReview{ + Spec: v1alpha1.ImageReviewSpec{ + Containers: imageReviewContainerSpecs, + Annotations: a.filterAnnotations(pod.Annotations), + Namespace: attributes.GetNamespace(), + }, + } + if err := a.admitPod(attributes, &imageReview); err != nil { + return admission.NewForbidden(attributes, err) + } + return nil +} + +func (a *imagePolicyWebhook) admitPod(attributes admission.Attributes, review *v1alpha1.ImageReview) error { + cacheKey, err := json.Marshal(review.Spec) + if err != nil { + return err + } + if entry, ok := a.responseCache.Get(string(cacheKey)); ok { + review.Status = entry.(v1alpha1.ImageReviewStatus) + } else { + result := a.webhook.WithExponentialBackoff(func() restclient.Result { + return a.webhook.RestClient.Post().Body(review).Do() + }) + + if err := result.Error(); err != nil { + return a.webhookError(attributes, err) + } + var statusCode int + if result.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 { + return a.webhookError(attributes, fmt.Errorf("Error contacting webhook: %d", statusCode)) + } + + if err := result.Into(review); err != nil { + return a.webhookError(attributes, err) + } + + a.responseCache.Add(string(cacheKey), review.Status, a.statusTTL(review.Status)) + } + + if !review.Status.Allowed { + if len(review.Status.Reason) > 0 { + return fmt.Errorf("image policy webook backend denied one or more images: %s", review.Status.Reason) + } + return errors.New("one or more images rejected by webhook backend") + } + + return nil +} + +// NewImagePolicyWebhook a new imagePolicyWebhook from the provided config file. +// The config file is specified by --admission-controller-config-file and has the +// following format for a webhook: +// +// { +// "imagePolicy": { +// "kubeConfigFile": "path/to/kubeconfig/for/backend", +// "allowTTL": 30, # time in s to cache approval +// "denyTTL": 30, # time in s to cache denial +// "retryBackoff": 500, # time in ms to wait between retries +// "defaultAllow": true # determines behavior if the webhook backend fails +// } +// } +// +// The config file may be json or yaml. +// +// The kubeconfig property refers to another file in the kubeconfig format which +// specifies how to connect to the webhook backend. +// +// The kubeconfig's cluster field is used to refer to the remote service, user refers to the returned authorizer. +// +// # clusters refers to the remote service. +// clusters: +// - name: name-of-remote-imagepolicy-service +// cluster: +// certificate-authority: /path/to/ca.pem # CA for verifying the remote service. +// server: https://images.example.com/policy # URL of remote service to query. Must use 'https'. +// +// # users refers to the API server's webhook configuration. +// users: +// - name: name-of-api-server +// user: +// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use +// client-key: /path/to/key.pem # key matching the cert +// +// For additional HTTP configuration, refer to the kubeconfig documentation +// http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html. +func NewImagePolicyWebhook(client clientset.Interface, configFile io.Reader) (admission.Interface, error) { + var config AdmissionConfig + d := yaml.NewYAMLOrJSONDecoder(configFile, 4096) + err := d.Decode(&config) + if err != nil { + return nil, err + } + + whConfig := config.ImagePolicyWebhook + if err := normalizeWebhookConfig(&whConfig); err != nil { + return nil, err + } + + gw, err := webhook.NewGenericWebhook(whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff) + if err != nil { + return nil, err + } + return &imagePolicyWebhook{ + Handler: admission.NewHandler(admission.Create, admission.Update), + webhook: gw, + responseCache: cache.NewLRUExpireCache(1024), + allowTTL: whConfig.AllowTTL, + denyTTL: whConfig.DenyTTL, + defaultAllow: whConfig.DefaultAllow, + }, nil +} diff --git a/plugin/pkg/admission/imagepolicy/admission_test.go b/plugin/pkg/admission/imagepolicy/admission_test.go new file mode 100644 index 00000000000..cb377bf6432 --- /dev/null +++ b/plugin/pkg/admission/imagepolicy/admission_test.go @@ -0,0 +1,944 @@ +/* +Copyright 2016 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. +*/ + +package imagepolicy + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "math/rand" + "net/http" + "net/http/httptest" + "reflect" + "strconv" + "testing" + "time" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/imagepolicy/v1alpha1" + "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api/v1" + + "fmt" + "io/ioutil" + "os" + "path/filepath" + "text/template" + + _ "k8s.io/kubernetes/pkg/apis/imagepolicy/install" +) + +const defaultConfigTmplJSON = ` +{ +"imagePolicy": { + "kubeConfigFile": "{{ .KubeConfig }}", + "allowTTL": {{ .AllowTTL }}, + "denyTTL": {{ .DenyTTL }}, + "retryBackoff": {{ .RetryBackoff }}, + "defaultAllow": {{ .DefaultAllow }} +} +} +` + +const defaultConfigTmplYAML = ` +imagePolicy: + kubeConfigFile: "{{ .KubeConfig }}" + allowTTL: {{ .AllowTTL }} + denyTTL: {{ .DenyTTL }} + retryBackoff: {{ .RetryBackoff }} + defaultAllow: {{ .DefaultAllow }} +` + +func TestNewFromConfig(t *testing.T) { + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + data := struct { + CA string + Cert string + Key string + }{ + CA: filepath.Join(dir, "ca.pem"), + Cert: filepath.Join(dir, "clientcert.pem"), + Key: filepath.Join(dir, "clientkey.pem"), + } + + files := []struct { + name string + data []byte + }{ + {data.CA, caCert}, + {data.Cert, clientCert}, + {data.Key, clientKey}, + } + for _, file := range files { + if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil { + t.Fatal(err) + } + } + + tests := []struct { + msg string + kubeConfigTmpl string + wantErr bool + }{ + { + msg: "a single cluster and single user", + kubeConfigTmpl: ` +clusters: +- cluster: + certificate-authority: {{ .CA }} + server: https://admission.example.com + name: foobar +users: +- name: a cluster + user: + client-certificate: {{ .Cert }} + client-key: {{ .Key }} +`, + wantErr: false, + }, + { + msg: "multiple clusters with no context", + kubeConfigTmpl: ` +clusters: +- cluster: + certificate-authority: {{ .CA }} + server: https://admission.example.com + name: foobar +- cluster: + certificate-authority: a bad certificate path + server: https://admission.example.com + name: barfoo +users: +- name: a name + user: + client-certificate: {{ .Cert }} + client-key: {{ .Key }} +`, + wantErr: false, + }, + { + msg: "multiple clusters with a context", + kubeConfigTmpl: ` +clusters: +- cluster: + certificate-authority: a bad certificate path + server: https://admission.example.com + name: foobar +- cluster: + certificate-authority: {{ .CA }} + server: https://admission.example.com + name: barfoo +users: +- name: a name + user: + client-certificate: {{ .Cert }} + client-key: {{ .Key }} +contexts: +- name: default + context: + cluster: barfoo + user: a name +current-context: default +`, + wantErr: false, + }, + { + msg: "cluster with bad certificate path specified", + kubeConfigTmpl: ` +clusters: +- cluster: + certificate-authority: a bad certificate path + server: https://admission.example.com + name: foobar +- cluster: + certificate-authority: {{ .CA }} + server: https://admission.example.com + name: barfoo +users: +- name: a name + user: + client-certificate: {{ .Cert }} + client-key: {{ .Key }} +contexts: +- name: default + context: + cluster: foobar + user: a name +current-context: default +`, + wantErr: true, + }, + } + + for _, tt := range tests { + // Use a closure so defer statements trigger between loop iterations. + err := func() error { + tempfile, err := ioutil.TempFile("", "") + if err != nil { + return err + } + p := tempfile.Name() + defer os.Remove(p) + + tmpl, err := template.New("test").Parse(tt.kubeConfigTmpl) + if err != nil { + return fmt.Errorf("failed to parse test template: %v", err) + } + if err := tmpl.Execute(tempfile, data); err != nil { + return fmt.Errorf("failed to execute test template: %v", err) + } + + tempconfigfile, err := ioutil.TempFile("", "") + if err != nil { + return err + } + pc := tempconfigfile.Name() + defer os.Remove(pc) + + configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplJSON) + if err != nil { + return fmt.Errorf("failed to parse test template: %v", err) + } + dataConfig := struct { + KubeConfig string + AllowTTL int + DenyTTL int + RetryBackoff int + DefaultAllow bool + }{ + KubeConfig: p, + AllowTTL: 500, + DenyTTL: 500, + RetryBackoff: 500, + DefaultAllow: true, + } + if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil { + return fmt.Errorf("failed to execute test template: %v", err) + } + + // Create a new admission controller + configFile, err := os.Open(pc) + if err != nil { + return fmt.Errorf("failed to read test config: %v", err) + } + + _, err = NewImagePolicyWebhook(fake.NewSimpleClientset(), configFile) + return err + }() + if err != nil && !tt.wantErr { + t.Errorf("failed to load plugin from config %q: %v", tt.msg, err) + } + if err == nil && tt.wantErr { + t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg) + } + } +} + +// Service mocks a remote service. +type Service interface { + Review(*v1alpha1.ImageReview) + HTTPStatusCode() int +} + +// NewTestServer wraps a Service as an httptest.Server. +func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) { + var tlsConfig *tls.Config + if cert != nil { + cert, err := tls.X509KeyPair(cert, key) + if err != nil { + return nil, err + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + + if caCert != nil { + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(caCert) + if tlsConfig == nil { + tlsConfig = &tls.Config{} + } + tlsConfig.ClientCAs = rootCAs + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + + serveHTTP := func(w http.ResponseWriter, r *http.Request) { + var review v1alpha1.ImageReview + if err := json.NewDecoder(r.Body).Decode(&review); err != nil { + http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) + return + } + if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 { + http.Error(w, "HTTP Error", s.HTTPStatusCode()) + return + } + s.Review(&review) + type status struct { + Allowed bool `json:"allowed"` + Reason string `json:"reason"` + } + resp := struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Status status `json:"status"` + }{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "ImageReview", + Status: status{review.Status.Allowed, review.Status.Reason}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + } + + server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP)) + server.TLS = tlsConfig + server.StartTLS() + return server, nil +} + +// A service that can be set to allow all or deny all authorization requests. +type mockService struct { + allow bool + statusCode int +} + +func (m *mockService) Review(r *v1alpha1.ImageReview) { + r.Status.Allowed = m.allow + + // hardcoded overrides + if r.Spec.Containers[0].Image == "good" { + r.Status.Allowed = true + } + + for _, c := range r.Spec.Containers { + if c.Image == "bad" { + r.Status.Allowed = false + } + } + + if !r.Status.Allowed { + r.Status.Reason = "not allowed" + } +} +func (m *mockService) Allow() { m.allow = true } +func (m *mockService) Deny() { m.allow = false } +func (m *mockService) HTTPStatusCode() int { return m.statusCode } + +// newImagePolicyWebhook creates a temporary kubeconfig file from the provided arguments and attempts to load +// a new newImagePolicyWebhook from it. +func newImagePolicyWebhook(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, defaultAllow bool) (*imagePolicyWebhook, error) { + tempfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, err + } + p := tempfile.Name() + defer os.Remove(p) + config := v1.Config{ + Clusters: []v1.NamedCluster{ + { + Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca}, + }, + }, + AuthInfos: []v1.NamedAuthInfo{ + { + AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey}, + }, + }, + } + if err := json.NewEncoder(tempfile).Encode(config); err != nil { + return nil, err + } + + tempconfigfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, err + } + pc := tempconfigfile.Name() + defer os.Remove(pc) + + configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplYAML) + if err != nil { + return nil, fmt.Errorf("failed to parse test template: %v", err) + } + dataConfig := struct { + KubeConfig string + AllowTTL int64 + DenyTTL int64 + RetryBackoff int64 + DefaultAllow bool + }{ + KubeConfig: p, + AllowTTL: cacheTime.Nanoseconds(), + DenyTTL: cacheTime.Nanoseconds(), + RetryBackoff: 0, + DefaultAllow: defaultAllow, + } + if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil { + return nil, fmt.Errorf("failed to execute test template: %v", err) + } + + // Create a new admission controller + configFile, err := os.Open(pc) + if err != nil { + return nil, fmt.Errorf("failed to read test config: %v", err) + } + wh, err := NewImagePolicyWebhook(fake.NewSimpleClientset(), configFile) + return wh.(*imagePolicyWebhook), err +} + +func TestTLSConfig(t *testing.T) { + tests := []struct { + test string + clientCert, clientKey, clientCA []byte + serverCert, serverKey, serverCA []byte + wantAllowed, wantErr bool + }{ + { + test: "TLS setup between client and server", + clientCert: clientCert, clientKey: clientKey, clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, serverCA: caCert, + wantAllowed: true, + }, + { + test: "Server does not require client auth", + clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, + wantAllowed: true, + }, + { + test: "Server does not require client auth, client provides it", + clientCert: clientCert, clientKey: clientKey, clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, + wantAllowed: true, + }, + { + test: "Client does not trust server", + clientCert: clientCert, clientKey: clientKey, + serverCert: serverCert, serverKey: serverKey, + wantErr: true, + }, + { + test: "Server does not trust client", + clientCert: clientCert, clientKey: clientKey, clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, serverCA: badCACert, + wantErr: true, + }, + { + // Plugin does not support insecure configurations. + test: "Server is using insecure connection", + wantErr: true, + }, + } + for _, tt := range tests { + // Use a closure so defer statements trigger between loop iterations. + func() { + service := new(mockService) + service.statusCode = 200 + + server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA) + if err != nil { + t.Errorf("%s: failed to create server: %v", tt.test, err) + return + } + defer server.Close() + + wh, err := newImagePolicyWebhook(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, -1, false) + if err != nil { + t.Errorf("%s: failed to create client: %v", tt.test, err) + return + } + pod := goodPod(strconv.Itoa(rand.Intn(1000))) + attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) + + // Allow all and see if we get an error. + service.Allow() + + err = wh.Admit(attr) + if tt.wantAllowed { + if err != nil { + t.Errorf("expected successful admission") + } + } else { + if err == nil { + t.Errorf("expected failed admission") + } + } + if tt.wantErr { + if err == nil { + t.Errorf("expected error making admission request: %v", err) + } + return + } + if err != nil { + t.Errorf("%s: failed to admit with AllowAll policy: %v", tt.test, err) + return + } + + service.Deny() + if err := wh.Admit(attr); err == nil { + t.Errorf("%s: incorrectly admitted with DenyAll policy", tt.test) + } + }() + } +} + +type webhookCacheTestCase struct { + statusCode int + expectedErr bool + expectedAuthorized bool + expectedCached bool +} + +func testWebhookCacheCases(t *testing.T, serv *mockService, wh *imagePolicyWebhook, attr admission.Attributes, tests []webhookCacheTestCase) { + for _, test := range tests { + serv.statusCode = test.statusCode + err := wh.Admit(attr) + authorized := err == nil + + if test.expectedErr && err == nil { + t.Errorf("Expected error") + } else if !test.expectedErr && err != nil { + t.Fatal(err) + } + if test.expectedAuthorized && !authorized { + if test.expectedCached { + t.Errorf("Webhook should have successful response cached, but authorizer reported unauthorized.") + } else { + t.Errorf("Webhook returned HTTP %d, but authorizer reported unauthorized.", test.statusCode) + } + } else if !test.expectedAuthorized && authorized { + t.Errorf("Webhook returned HTTP %d, but authorizer reported success.", test.statusCode) + } + } +} + +// TestWebhookCache verifies that error responses from the server are not +// cached, but successful responses are. +func TestWebhookCache(t *testing.T) { + serv := new(mockService) + s, err := NewTestServer(serv, serverCert, serverKey, caCert) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + // Create an admission controller that caches successful responses. + wh, err := newImagePolicyWebhook(s.URL, clientCert, clientKey, caCert, 200, false) + if err != nil { + t.Fatal(err) + } + + tests := []webhookCacheTestCase{ + {statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false}, + {statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCached: false}, + {statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCached: false}, + {statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCached: false}, + {statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false}, + {statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true}, + } + + attr := admission.NewAttributesRecord(goodPod("test"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) + + serv.allow = true + + testWebhookCacheCases(t, serv, wh, attr, tests) + + // For a different request, webhook should be called again. + tests = []webhookCacheTestCase{ + {statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false}, + {statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false}, + {statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true}, + } + attr = admission.NewAttributesRecord(goodPod("test2"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) + + testWebhookCacheCases(t, serv, wh, attr, tests) +} + +func TestContainerCombinations(t *testing.T) { + tests := []struct { + test string + pod *api.Pod + wantAllowed, wantErr bool + }{ + { + test: "Single container allowed", + pod: goodPod("good"), + wantAllowed: true, + }, + { + test: "Single container denied", + pod: goodPod("bad"), + wantAllowed: false, + wantErr: true, + }, + { + test: "One good container, one bad", + pod: &api.Pod{ + Spec: api.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + Image: "bad", + SecurityContext: &api.SecurityContext{}, + }, + { + Image: "good", + SecurityContext: &api.SecurityContext{}, + }, + }, + }, + }, + wantAllowed: false, + wantErr: true, + }, + { + test: "Multiple good containers", + pod: &api.Pod{ + Spec: api.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + Image: "good", + SecurityContext: &api.SecurityContext{}, + }, + { + Image: "good", + SecurityContext: &api.SecurityContext{}, + }, + }, + }, + }, + wantAllowed: true, + wantErr: false, + }, + { + test: "Multiple bad containers", + pod: &api.Pod{ + Spec: api.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + Image: "bad", + SecurityContext: &api.SecurityContext{}, + }, + { + Image: "bad", + SecurityContext: &api.SecurityContext{}, + }, + }, + }, + }, + wantAllowed: false, + wantErr: true, + }, + { + test: "Good container, bad init container", + pod: &api.Pod{ + Spec: api.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + Image: "good", + SecurityContext: &api.SecurityContext{}, + }, + }, + InitContainers: []api.Container{ + { + Image: "bad", + SecurityContext: &api.SecurityContext{}, + }, + }, + }, + }, + wantAllowed: false, + wantErr: true, + }, + { + test: "Bad container, good init container", + pod: &api.Pod{ + Spec: api.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + Image: "bad", + SecurityContext: &api.SecurityContext{}, + }, + }, + InitContainers: []api.Container{ + { + Image: "good", + SecurityContext: &api.SecurityContext{}, + }, + }, + }, + }, + wantAllowed: false, + wantErr: true, + }, + { + test: "Good container, good init container", + pod: &api.Pod{ + Spec: api.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + Image: "good", + SecurityContext: &api.SecurityContext{}, + }, + }, + InitContainers: []api.Container{ + { + Image: "good", + SecurityContext: &api.SecurityContext{}, + }, + }, + }, + }, + wantAllowed: true, + wantErr: false, + }, + } + for _, tt := range tests { + // Use a closure so defer statements trigger between loop iterations. + func() { + service := new(mockService) + service.statusCode = 200 + + server, err := NewTestServer(service, serverCert, serverKey, caCert) + if err != nil { + t.Errorf("%s: failed to create server: %v", tt.test, err) + return + } + defer server.Close() + + wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, false) + if err != nil { + t.Errorf("%s: failed to create client: %v", tt.test, err) + return + } + + attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) + + err = wh.Admit(attr) + if tt.wantAllowed { + if err != nil { + t.Errorf("expected successful admission: %s", tt.test) + } + } else { + if err == nil { + t.Errorf("expected failed admission: %s", tt.test) + } + } + if tt.wantErr { + if err == nil { + t.Errorf("expected error making admission request: %v", err) + } + return + } + if err != nil { + t.Errorf("%s: failed to admit: %v", tt.test, err) + return + } + }() + } +} + +func TestDefaultAllow(t *testing.T) { + tests := []struct { + test string + pod *api.Pod + wantAllowed, wantErr, defaultAllow bool + }{ + { + test: "DefaultAllow = true, backend unreachable, bad image", + pod: goodPod("bad"), + defaultAllow: true, + wantAllowed: true, + }, + { + test: "DefaultAllow = true, backend unreachable, good image", + pod: goodPod("good"), + defaultAllow: true, + wantAllowed: true, + }, + { + test: "DefaultAllow = false, backend unreachable, good image", + pod: goodPod("good"), + defaultAllow: false, + wantAllowed: false, + wantErr: true, + }, + { + test: "DefaultAllow = false, backend unreachable, bad image", + pod: goodPod("bad"), + defaultAllow: false, + wantAllowed: false, + wantErr: true, + }, + } + for _, tt := range tests { + // Use a closure so defer statements trigger between loop iterations. + func() { + service := new(mockService) + service.statusCode = 500 + + server, err := NewTestServer(service, serverCert, serverKey, caCert) + if err != nil { + t.Errorf("%s: failed to create server: %v", tt.test, err) + return + } + defer server.Close() + + wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, tt.defaultAllow) + if err != nil { + t.Errorf("%s: failed to create client: %v", tt.test, err) + return + } + + attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) + + err = wh.Admit(attr) + if tt.wantAllowed { + if err != nil { + t.Errorf("expected successful admission") + } + } else { + if err == nil { + t.Errorf("expected failed admission") + } + } + if tt.wantErr { + if err == nil { + t.Errorf("expected error making admission request: %v", err) + } + return + } + if err != nil { + t.Errorf("%s: failed to admit: %v", tt.test, err) + return + } + }() + } +} + +// A service that can record annotations sent to it +type annotationService struct { + annotations map[string]string +} + +func (a *annotationService) Review(r *v1alpha1.ImageReview) { + a.annotations = make(map[string]string) + for k, v := range r.Spec.Annotations { + a.annotations[k] = v + } + r.Status.Allowed = true +} +func (a *annotationService) HTTPStatusCode() int { return 200 } +func (a *annotationService) Annotations() map[string]string { return a.annotations } + +func TestAnnotationFiltering(t *testing.T) { + tests := []struct { + test string + annotations map[string]string + outAnnotations map[string]string + }{ + { + test: "all annotations filtered out", + annotations: map[string]string{ + "test": "test", + "another": "annotation", + "": "", + }, + outAnnotations: map[string]string{}, + }, + { + test: "image-policy annotations allowed", + annotations: map[string]string{ + "my.image-policy.k8s.io/test": "test", + "other.image-policy.k8s.io/test2": "annotation", + "test": "test", + "another": "another", + "": "", + }, + outAnnotations: map[string]string{ + "my.image-policy.k8s.io/test": "test", + "other.image-policy.k8s.io/test2": "annotation", + }, + }, + } + for _, tt := range tests { + // Use a closure so defer statements trigger between loop iterations. + func() { + service := new(annotationService) + + server, err := NewTestServer(service, serverCert, serverKey, caCert) + if err != nil { + t.Errorf("%s: failed to create server: %v", tt.test, err) + return + } + defer server.Close() + + wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, true) + if err != nil { + t.Errorf("%s: failed to create client: %v", tt.test, err) + return + } + + pod := goodPod("test") + pod.Annotations = tt.annotations + + attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) + + err = wh.Admit(attr) + if err != nil { + t.Errorf("expected successful admission") + } + + if !reflect.DeepEqual(tt.outAnnotations, service.Annotations()) { + t.Errorf("expected annotations sent to webhook: %v to match expected: %v", service.Annotations(), tt.outAnnotations) + } + + }() + } +} + +func goodPod(containerID string) *api.Pod { + return &api.Pod{ + Spec: api.PodSpec{ + ServiceAccountName: "default", + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + Image: containerID, + SecurityContext: &api.SecurityContext{}, + }, + }, + }, + } +} diff --git a/plugin/pkg/admission/imagepolicy/certs_test.go b/plugin/pkg/admission/imagepolicy/certs_test.go new file mode 100644 index 00000000000..2a9177ee32f --- /dev/null +++ b/plugin/pkg/admission/imagepolicy/certs_test.go @@ -0,0 +1,211 @@ +/* +Copyright 2016 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. +*/ + +// This file was generated using openssl by the gencerts.sh script +// and holds raw certificates for the imagepolicy webhook tests. + +package imagepolicy + +var caKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAoKjaP9PtRAGRNCx8z+0LTGt2eEduqElcPrm8EvlBwn3dnLFo +55x+Tejb6ysQsyy1BKI0dRdX4tNSAgFFFaIVcsOo9kGtPq7QsSd4VWViNE3L5zJA ++0X2ztHBkPlQXwDrtArsNKxwcpyHP9sXE05BN36XBjAz2XkusTkFrdJ/PzjZhlb4 +9i9gTZ0bJbexQ1+dfZX2WpY70JypYnKrbV1dLj5ORb65SC8IWZcG/ouqLWAN+lT+ +eug8P6PjoOQWs3qsl0bSAtAdiYcwXKtPiBEWPJe24ACywyE+8jVzmIJqAm0U1V8k +GTHzjmSRwzgX/VN5JMri/nxNIW5UsbhHzYHfjQIDAQABAoIBAQCIeAWz1Bwl+ULT +U7rNkChZyKrAbsUDdBVEPtcQMuR2Bh5Z/KUEoHz1RwiP0WwFFsPI5NO0ZpjD1wdB +Jrz9LEoVyzfZvl4f8bTZ1pIzz8PEdBTxFVH3Xy3P7oMC15Q6rviIXgLYl2WJJYcJ +adxHDOD+96vnmMhiQbq01aAKT9TA6PvXXDusfadMQ+il+mEbeZz4aNYBk9u+34Co +aQTNwlLft5anW2820IMJdJR/bFjyX71cPID1rIjw4VOQZExIpIEnuHPiulyE4EvJ +hvvVKAm0dRjHg39cz0eAQ6PntX3DUvjNfcLLrj7sQxLco1cnAKZxhpZ8ajtvynr5 +pF2d5xYBAoGBAM8y/e5+raHTLHEKZUc0vekUey3fc4aRqptyAKTS0ZvOYBXg4Vhl +mOK7066IEqwF4UHGmQqW6D5HstqPGx0uN0d9IyImUqDp0JotdFSZMEMQkYLyFD+r +J7O2nOO6E4SOxXO9/q9iSB+G/qgl6LS3O9+58uHTYEbUommiDZ6a18qBAoGBAMZ/ +xSGMa3b6vrU3rUTEh+xBh6YRVNYAxWwpGg2sO0k2brT3SxSMCrx1wvNGY+k7XNx0 +JJfZQDC/wlR0rcVTnPCi/cE9FTUlh23xXCPRlxwc4vLly+7yU95LhAO+N9XAwsrs +OIi4lR57jxoLNO2ofoAVMvllkE5Eo5W6lOPR2xcNAoGAV1Tv0OFV//pJJhAypfOm +BCLc1HX1dIfbOA+yE8bEEH7I4w/ZC3AvI4n1a//wls8Xpai2gs8ebnm7+gENdZww +MpKdB1zNwQMsKH/2I146CFpoap/sRvW2EzpqIFYiueGPefxf575uFdPJbEgmMF13 +ABKZO/PjBZfEKO/j+7DaOYECgYBYX+Zqa1QlIrnpgKJZ7Y3+d6ZnH2w/4xQCdcIt +uDKlA+ECHN+GhFr7UQq8uOgenNlZJTRtjsHvclCYvWHoarOCx25mrEVW5iCHqF+3 +asb2Mz4vmnPTLHx+iex6piPBvRJ8ufLpnBR3/9bUZ4znCo9XgxiwxLEcx551OR60 +12fNuQKBgC1fkqgtDDxQzrabSmmiqXthcPXxFdsYqnSNlFgba0uaAp9LREztSrX8 +QhwSoSwHVmjBvR6SybLYdsZ9Efj/w7XBejOOcS44MOoHYYFdsP7W47Ao5QFqvDoI +oqyQ1R73cF9WX6obRQwH4P3DvcsBebOjvjMX9mljKtpJMc9KqrGc +-----END RSA PRIVATE KEY-----`) + +var caCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDFzCCAf+gAwIBAgIJAJlL10mfdZraMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV +BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5 +MDA1MjgyMDIyNTBaMCExHzAdBgNVBAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgqNo/0+1EAZE0LHzP7QtM +a3Z4R26oSVw+ubwS+UHCfd2csWjnnH5N6NvrKxCzLLUEojR1F1fi01ICAUUVohVy +w6j2Qa0+rtCxJ3hVZWI0TcvnMkD7RfbO0cGQ+VBfAOu0Cuw0rHBynIc/2xcTTkE3 +fpcGMDPZeS6xOQWt0n8/ONmGVvj2L2BNnRslt7FDX519lfZaljvQnKlicqttXV0u +Pk5FvrlILwhZlwb+i6otYA36VP566Dw/o+Og5BazeqyXRtIC0B2JhzBcq0+IERY8 +l7bgALLDIT7yNXOYgmoCbRTVXyQZMfOOZJHDOBf9U3kkyuL+fE0hblSxuEfNgd+N +AgMBAAGjUDBOMB0GA1UdDgQWBBSx2m5pJoFpdGDmOzSVl29jkheQFTAfBgNVHSME +GDAWgBSx2m5pJoFpdGDmOzSVl29jkheQFTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBe6tZzmOQKt8fTsnDDKvEjSwK2Pb91R5tkwmIhdpTjmAgC+Zkk +kSihR9sZIxdRC4wlbuorRl8BjhX5I8Kr3FWdDhOrIhicp7CIrxPiFh6+ZLSOj3o9 +pQ6SriIopjXCHvl5XjzKxLg/uQpzui/YUtfqffCRB4EccOsjlyUanK5rjMLBMLCn +2LadiRB2Q/cC9fYigczETACDjq5vzp6I9eqwpCTmv/+4bFncW+VBD4touaJc8FKf +ljW5xekKRh4uzP85X7rEgrFen/my5Fs/cylkFvYIiZwgn6NLgW3BNi+m31XIfU0S +xIbgh4UH0dwc6Zk8WUwFud4GXj6OyGneMGKB +-----END CERTIFICATE-----`) + +var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAnKpC89q4/H+Xg91xI+GLhkrpJrO4n3nw0+/EQUoF9qwLtEDk +mJp6ymUulwJgfvJwHOsUYqQB6jMKfXyqeSR24ssjjF9LTKhaQMZOGcW5Mshi04Ie +USX93wDZwbWwqihVSqWaMpmf3JByeldnXNtc29Ik6NwqZcNWW5kEsSszheLhOU4i +ZcRUlovwMYhHX37vQCQ1aygMaIMgBOb/vogSNxumqPKS4WdWsjss6LmEPnm350e+ ++9cb6RAfrDlOaj32VbLEp500SfBpZeCuyc5v81X12HT4V0qmsqIZ79tIgrqAaPxE +D/HJXPpH64EAr23bR9dMPLXTh6w2yYWu+NGYywIDAQABAoIBAQCE/CZPN1gVxf0I +i12x9o/oVAhruN08Sld6oCm4viwnws1AmmExhNg8m/0bZIIi4Ir4kThBrzSM5/y8 +nqlaofBk/cjULEQP80yBdZPwXp2hlOYG4on3mkdRGDjALQmktw4HimFFGJDRuq/i +V/U+plrBojWAkPtQXKsen9qSxbg7qhI6KZyUQKExIHhsCfmE1ZzGx+/bgLVJEagi +7zzZdAj2BzdoCk8yySAAsZG+pNSnd8gs5EzzRJ1RXanwxPSeEG/guX9YhLgLhhFu +XzXngJDKVVhz4F2TfxtqIvZYvTMNh0R1OE0OUO2P88M837KKk5BHvW9oqYKZTUFV +MC9k5No5AoGBAMtUBp8UcYZy+yetOAK2iGaEYwuWx8vwjY0c1POWun2Hny0nYxTQ +WxXXqKaJydxZ+DlD3XuRKmMlKZQsp+bzuL5ukWN/ipO5tgQQfuKOZqVwvL19GkFi ++Qr70G/TvYT/rv6A4s6XqbG4xt+7c2gf/XSghyoIyq1uwOcNNtrMdM/tAoGBAMU/ +tYc4d+vAl7hd8TwhFiZiC3N84C1HwsPVj38uqQI/j8boB21Bhpw6HHzq+VdVPfvp +zk5e8AiQdSpitM7pBVmLpoRdTQjdlUDFRUi4TdJwfp5P7dXM8D6swNQ9f9w180na +5ewu16PSC+sh19wAl04KwOmiDqZujJrBgWnFcESXAoGBALGofoybAUK3zqlxWcJN +GUtyG1Sx72tLiXMmIQ+hwNsUGEoM4y75isy//ZVeSammVxQ6Lxjb00yD2RumFSLg +C6kg1Ro6A6xmFRriCuwL/rZJljB/UeSWBQLK2eoL+clu2sl3djWLIPOvft1YXVM6 +uGwiI1fgDK+TWSvJSQfOo7ZVAoGBAK+A6DvQeqNBUb2xmJsvtU2hnx661Zx0ZU9q +DavUEHz3oS4R9cm4q9UFv6NGT2Tta6FhfzcsMdbs8dMs0EPqAeCS6S6M9aYVwl9H +J0Z09olvnrmt1KiPGJQrkcdGkSWWu0nTgxCK/UO9+OzVyALwY7AE0XEPyIk9g82O +r181VZcxAoGANY2QGYrNtfa++o2B0O4qskKxhYEeCnZPptmjVO0oHOx2YSDQXK3K +B0evCQ7ylvMnobNLjp9bqD14a0M86QjRlpSg1vHUhBsETZICc+E0UgV28CdWgYtt +urARDE9ZpLVSRfPVAitC1I76pZwevsbQ9TeS2p0cWQpYYKmBtGpkdug= +-----END RSA PRIVATE KEY-----`) + +var badCACert = []byte(`-----BEGIN CERTIFICATE----- +MIIDFzCCAf+gAwIBAgIJANQEJyMW4HFZMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV +BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5 +MDA1MjgyMDIyNTBaMCExHzAdBgNVBAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcqkLz2rj8f5eD3XEj4YuG +Sukms7ifefDT78RBSgX2rAu0QOSYmnrKZS6XAmB+8nAc6xRipAHqMwp9fKp5JHbi +yyOMX0tMqFpAxk4ZxbkyyGLTgh5RJf3fANnBtbCqKFVKpZoymZ/ckHJ6V2dc21zb +0iTo3Cplw1ZbmQSxKzOF4uE5TiJlxFSWi/AxiEdffu9AJDVrKAxogyAE5v++iBI3 +G6ao8pLhZ1ayOyzouYQ+ebfnR7771xvpEB+sOU5qPfZVssSnnTRJ8Gll4K7Jzm/z +VfXYdPhXSqayohnv20iCuoBo/EQP8clc+kfrgQCvbdtH10w8tdOHrDbJha740ZjL +AgMBAAGjUDBOMB0GA1UdDgQWBBRjFVG818hHK+HSEhdz+gPwSKa4kzAfBgNVHSME +GDAWgBRjFVG818hHK+HSEhdz+gPwSKa4kzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBBCl0UJq0iLy/dvym79mnoPZ1KPhS2WnQB5ZLzJwL26ePkr8j8 +G/1AOVPu73hovJx51b+T7ZhTgtmAEwqpRHBxRQ0+Yf973YOVJYp4QFGWDnueurzv +bCsnZEPkQtccHzZxT3fUsM6Ejy99j0WBNmvfAj1X7yNaN5EZw6kvuaDDda3I7WNM +0eGy8aoAcPJZkYfZb39VDq/qJn+bVsAJdUaXt/FkDZBJl6XzoGjC/webjRJOpkgN +vgjJDhhQ8LlHFiq+lXIiK4Y55RBWG3iXGTM8W3fjZYTNvH7FlGyuRD4Y4hyaYXTP ++PoFWuDZM89EAyICr0yyTc8mkdrAEM/Lj9GO +-----END CERTIFICATE-----`) + +var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAyhmjG7BJCGwuf1FyHJtq9iUXZ3oymtrOHdaSAcsCSxFrUJTH +riPOe9d1ahH7bvsZycnzh7/pABdTDUdStiR8/1KUYt8PjjosrmmYyupqNPq+wkBD +EmKa+4voR2EBgXbIGghx8e++KmmNnSCNk6B8m2EJR0fn9zPnoY3uHNogKjCICt19 +g+uipuwZco7yTu3e40LwpIVmA8SsrM0S/CaZqSmtIClSwv7YDvreUd6FuI/GT0cj +NMPRuSdfohBxGz6R7Cml7qP4AYKajjl+08mRYv3o+hVclXUltcRmTnJanYGmGS3k +C7KiE2sHINzF2qUUAoO+yXpMx7QtK+NS0PhjmQIDAQABAoIBAFOicmZ1+HM82a0k +llWSV5xPUzUmU6TT4bJlZnzJd0R7i+6H8250MPH9AwEHOgb+cPiZ02cdGx5HiL4Z +AviPdw7uLKwR5U0VdAIlfu6SPat5DNI0Z81G8x4gEtrfIRFjh4GGdykI7qh8j/cz +ToOGSaq/aGiQMEWTvEqWArD7742lVHE4/1bM3GuKV8shy31zfw0d9RCCy1GdBR75 +zZ1w4zKL55DM3PC73Ndy2IcrViVXVAgfqD0xxKwQW1qoENgThueALj3PkU1XaKxI +nOdztt1fBFpcSHyFBkJ1sexumnssMRXSVcJ/0D5F2T4QPUnWBM0oSzoyioAab4RP +8XrZwAECgYEA/eFjNgCeHztXgS3YRC/RddLOtobrerYKN7vA64ou5VUCqEQ9rfQE +MbmKdZdiFVNJI0JrPq8Gx39ME9g2OLTVVqdtlm6JYjy5CHdUXHIHObo9oz7Uueos +TdeCf0LFvEUNXvbGIP5KqcdVi+wekauHMqXGQYTNa6bar/FE99MdyAECgYEAy8mU +tCjm4QsuKsdku5bDHGv56ZN9DkWd7Lcjie5otElwH9bKfIQ2lUYyoUAIa0rEJ9Ya +7vuAZ2bX7od9s8Jkci91ONDWxdy361SRZcbpuqgQKKVRuzGlfamufyW4sStbXY1k ++zeQxyWGJHhhLWpapzca89RELGZSkbIMVVIT25kCgYEA7EUYboZuoYQ5cGf476RM +28kfRXEUrvPBWJLr/IhyEk1mFrDDciM40AnrWHpU9qG23BCQ/BopRforFADQnT91 +l5pje29NfdYjIUTkhtA79zZi7IyprofHSX453TOIECl3QxyH0Oa3F4ACFiDdZhXq +0XDDq+/quLfkp37y/2xDOAECgYEAmi55g5UumTWMSHFzlToLhIVtH3unMhUZ1u74 +xHLMZRrq6ivoJy0g3u+tfrKjrAl1P26OEiHWlGULGj0Ireh1dq7RUZsv46OKw1HI +b+h/Den5z8bEf4ygWOL4UtqHUgQrrCw+KpNvxjxtsUoiu+mrjLf0fGYs7iq8bd73 +1dWzkIECgYEAi6P/LzMC6orbyONmwlscqO1Ili8ZBkUjJ/wThkiNMMA3pyKmb68W +yt56Yh0rs+WnuVUN90cG87k+CY35dQ7FAOVUJi9LWGA3Oq9fGkoOB7f4dzaUu/rB +dtit2KPCxiKpZsxqSf4+S8AXYF48abNPLYK3DCCSqAah09gYOrqYlW4= +-----END RSA PRIVATE KEY-----`) + +var serverCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDCzCCAfOgAwIBAgIJAMvo2rkGpEUQMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV +BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5 +MDA1MjgyMDIyNTBaMCUxIzAhBgNVBAMUGndlYmhvb2tfaW1hZ2Vwb2xpY3lfc2Vy +dmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyhmjG7BJCGwuf1Fy +HJtq9iUXZ3oymtrOHdaSAcsCSxFrUJTHriPOe9d1ahH7bvsZycnzh7/pABdTDUdS +tiR8/1KUYt8PjjosrmmYyupqNPq+wkBDEmKa+4voR2EBgXbIGghx8e++KmmNnSCN +k6B8m2EJR0fn9zPnoY3uHNogKjCICt19g+uipuwZco7yTu3e40LwpIVmA8SsrM0S +/CaZqSmtIClSwv7YDvreUd6FuI/GT0cjNMPRuSdfohBxGz6R7Cml7qP4AYKajjl+ +08mRYv3o+hVclXUltcRmTnJanYGmGS3kC7KiE2sHINzF2qUUAoO+yXpMx7QtK+NS +0PhjmQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAK +BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCF +xaS/KIijKDLbaL/P7AxhnAta8jYSEzL66WTaYV4GeRhLtX/vPUV9gzPWnkNr0TBM +lS+Q0KDxh17rJ/MrWwrMSwsgKZahTR+7mSHiXrIlHcnHXXSvhnoXu8VDu8goqOEI +5yRHt6plzmFZEwVi/hSmIAuQjmyjOk2dc/ZKI0fMExKhnVms8AoztjAMbt3TFMTK +Kk7bVGPblFsXiVPhRlzbLbh5i/PvHHf+12ACrVxoxOOQUmuXy1DPxmkk7jP3FIsE ++rnyWnfmGS5sW8oMkj2nFYIh3LehADsMS9s7JVlJk/loNJDA9Yn2fev/vRKck8RZ +siw54G4e+6nKpY5BAY1M +-----END CERTIFICATE-----`) + +var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3IOqCz88jTQpsGIBFTdjbqBg+0NFeym3OEl8zLfzkLQuZieO +3AoFMiLaeYgC4m9BBsdJWSXRzWcqgVWIY8KU7c2SPfErlhP86VFoD0RKHJxwRVh0 +y70WyK8+CzzwrrPpWydgtAwbm9F+0v/zdcCL0TEL2/MYgCc97mSGwtTRaW4bqq6V +MWMHBcOu44dHq8+CF8ixxk0WSBl2oocXnF7QdEA15iuOM5hacLB0fyH4T3NM54lO +rOSXUMUuysougSrMcCPv3esFlv4TVUkldwu73jWx+Wja0gNXlnmgU2lqFdM+PsVT +DPMzoHTEhIGPIWO5anYR5Qv0SmX3nXkNcx9QDQIDAQABAoIBADblRCC2pmFUmghB +7ZkVh9hTbrE+Zv6pPOZzTPE93hGo+WAO+v6GNBLuIEte87DhF2QTmovp4VfsFeXK +oECNgTvOEFkBP+OFqFGBJZGfY3/J5h0tTy4lLZXaImzzx8sGGNLLc8R+uyTIO3VV +qIso2uXB+vzPgMrueflt5yp7hoJjI0c+qEktUg5n+WJFAFteI9LCngN+xwRWVEgp +rjKVPcT9zio8tLJOhcSPA7q6lORUkwbPWHyNDpamvldnqjhgp5Ceq5f/qfoWPzvM +H5o72Ax2WduxST+P+hCOqZReUmTaGzAKb5rJwdEpmbnDZ3kSR08aT/40m/EG1SvQ +pi0b3QECgYEA/mRGIjaYPQr+tw3Sz8g76t3PYfrglro60HdLBn2IUpj2sEpazNId +2aPFPb58whL+VPmUfXbpPH+wW/+wWpRw4MraFkJanbOjDiEGXK5ZoUQIDZJWUSwf +oCge5uacU69weC67UyPYmK1e+A/gaFw1Dz729jLxtB3rGWKxEGbWEc0CgYEA3eiP +hv0GxbdEEbSfQoSPKbBHGI9spaqAIcqL+dSsx3m6Ckqx0El/xi9mQkITgqs2gyqI +o2T/3yDli9oF4+3Plz0wrZ11auOWX+nhKfACtF679I1PL0UOavXF0FVgOfwOIqdG +jp4QQV7USkbTP9ZOHo90Y8G4rmTEdMZ/VsH490ECgYEA8u/bsiyk8haf7Tx8SAWW +gtLUi2NEO20ZYZ+qvEYBe6+sVeqMD/HQo9ksMazKA6ST0Z6O2cpHLolaaGEjjz0X +FvVhk8RGOTglzQZoxvWRjtojPqKzX81dXlsyN5ufSqPOKlemeN1QqW1XtlmjGsaD +vU2KFs/L1xCDRbjkEx/B6zkCgYBmqeE9InKvpknnpxjHPWy+bL93rWMmgesltv9r +ZelJoBdiC4yYQGjM18EHhmpgWbWumU79yQxXvnB0czmmaa9Q2Q5cRCy+duxrE1kI +ffHCYNG0ImwwAlLZSTtrVxRdvy8K+Ti7YoVCuQyeEIZLUmpx2QyP2mAGzrfVDsB6 +8uKsAQKBgQDO+PmADra91NKJP1iVuvOK8iEy/Z14L03uKtF3X9u8vLdzQZa/Q/P9 +hXOX9ovFwSBQOOfgb+/+QRuPL4xxi1J8CFwrSWCEeFgrDijl9DS6aNY6BWHDA8p6 +8V7Adb04cnenj8QjYYN8/mqsQlHSoAIxeAlUoJpq+pk7O8PAfbjgMw== +-----END RSA PRIVATE KEY-----`) + +var clientCert = []byte(`-----BEGIN CERTIFICATE----- +MIIC+jCCAeKgAwIBAgIJAMvo2rkGpEURMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV +BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5 +MDA1MjgyMDIyNTBaMCUxIzAhBgNVBAMUGndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2xp +ZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3IOqCz88jTQpsGIB +FTdjbqBg+0NFeym3OEl8zLfzkLQuZieO3AoFMiLaeYgC4m9BBsdJWSXRzWcqgVWI +Y8KU7c2SPfErlhP86VFoD0RKHJxwRVh0y70WyK8+CzzwrrPpWydgtAwbm9F+0v/z +dcCL0TEL2/MYgCc97mSGwtTRaW4bqq6VMWMHBcOu44dHq8+CF8ixxk0WSBl2oocX +nF7QdEA15iuOM5hacLB0fyH4T3NM54lOrOSXUMUuysougSrMcCPv3esFlv4TVUkl +dwu73jWx+Wja0gNXlnmgU2lqFdM+PsVTDPMzoHTEhIGPIWO5anYR5Qv0SmX3nXkN +cx9QDQIDAQABoy8wLTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAK +BggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAkHIhrPfRROhzLg2hRZz5/7Kw +3V0/Y0XS91YU3rew+c2k++bLp1INzpWxfB6gbSC6bTOgn/seIDvxwJ2g5DRdOxU/ +Elcpqg1hTCVfpmra9PCniMzZuP7lsz8sJKj6FgE6ElJ1S74FW/CYz/jA+76LLot4 +JwGkCJHzyLgFPBEOjJ/mLYSM/SDzHU5E+NHXVaKz4MjM3JwycN/juqi4ikAcZEBW +1HmpcHKBedAwlCM90zlvG2SL4sFRp/clMbntRdmh5L+/1F6aP82PO3iuvXtXP48d +NtjboxP3IV2eY5iUle8BOQ9CnFQs4wsF1LxTMNACypQyFinMsHrCpwrB3i4VvA== +-----END CERTIFICATE-----`) diff --git a/plugin/pkg/admission/imagepolicy/config.go b/plugin/pkg/admission/imagepolicy/config.go new file mode 100644 index 00000000000..5c0e8298e3b --- /dev/null +++ b/plugin/pkg/admission/imagepolicy/config.go @@ -0,0 +1,93 @@ +/* +Copyright 2016 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. +*/ + +// Package imagepolicy contains an admission controller that configures a webhook to which policy +// decisions are delegated. +package imagepolicy + +import ( + "fmt" + "time" + + "github.com/golang/glog" +) + +const ( + defaultRetryBackoff = time.Duration(500) * time.Millisecond + minRetryBackoff = time.Duration(1) + maxRetryBackoff = time.Duration(5) * time.Minute + defaultAllowTTL = time.Duration(5) * time.Minute + defaultDenyTTL = time.Duration(30) * time.Second + minAllowTTL = time.Duration(1) * time.Second + maxAllowTTL = time.Duration(30) * time.Minute + minDenyTTL = time.Duration(1) * time.Second + maxDenyTTL = time.Duration(30) * time.Minute + useDefault = time.Duration(0) //sentinel for using default TTL + disableTTL = time.Duration(-1) //sentinel for disabling a TTL +) + +// imagePolicyWebhookConfig holds config data for imagePolicyWebhook +type imagePolicyWebhookConfig struct { + KubeConfigFile string `json:"kubeConfigFile"` + AllowTTL time.Duration `json:"allowTTL"` + DenyTTL time.Duration `json:"denyTTL"` + RetryBackoff time.Duration `json:"retryBackoff"` + DefaultAllow bool `json:"defaultAllow"` +} + +// AdmissionConfig holds config data for admission controllers +type AdmissionConfig struct { + ImagePolicyWebhook imagePolicyWebhookConfig `json:"imagePolicy"` +} + +func normalizeWebhookConfig(config *imagePolicyWebhookConfig) (err error) { + config.RetryBackoff, err = normalizeConfigDuration("backoff", time.Millisecond, config.RetryBackoff, minRetryBackoff, maxRetryBackoff, defaultRetryBackoff) + if err != nil { + return err + } + config.AllowTTL, err = normalizeConfigDuration("allow cache", time.Second, config.AllowTTL, minAllowTTL, maxAllowTTL, defaultAllowTTL) + if err != nil { + return err + } + config.DenyTTL, err = normalizeConfigDuration("deny cache", time.Second, config.DenyTTL, minDenyTTL, maxDenyTTL, defaultDenyTTL) + if err != nil { + return err + } + return nil +} + +func normalizeConfigDuration(name string, scale, value, min, max, defaultValue time.Duration) (time.Duration, error) { + // disable with -1 sentinel + if value == disableTTL { + glog.V(2).Infof("image policy webhook %s disabled", name) + return time.Duration(0), nil + } + + // use defualt with 0 sentinel + if value == useDefault { + glog.V(2).Infof("image policy webhook %s using default value", name) + return defaultValue, nil + } + + // convert to s; unmarshalling gives ns + value *= scale + + // check value is within range + if value <= min || value > max { + return value, fmt.Errorf("valid value is between %v and %v, got %v", min, max, value) + } + return value, nil +} diff --git a/plugin/pkg/admission/imagepolicy/config_test.go b/plugin/pkg/admission/imagepolicy/config_test.go new file mode 100644 index 00000000000..9f09d181448 --- /dev/null +++ b/plugin/pkg/admission/imagepolicy/config_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2016 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. +*/ + +package imagepolicy + +import ( + "reflect" + "testing" + "time" +) + +func TestConfigNormalization(t *testing.T) { + tests := []struct { + test string + config imagePolicyWebhookConfig + normalizedConfig imagePolicyWebhookConfig + wantErr bool + }{ + { + test: "config within normal ranges", + config: imagePolicyWebhookConfig{ + AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second, + DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second, + RetryBackoff: ((minRetryBackoff + maxRetryBackoff) / 2) / time.Millisecond, + }, + normalizedConfig: imagePolicyWebhookConfig{ + AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second * time.Second, + DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second * time.Second, + RetryBackoff: (minRetryBackoff + maxRetryBackoff) / 2, + }, + wantErr: false, + }, + { + test: "config below normal ranges, error", + config: imagePolicyWebhookConfig{ + AllowTTL: minAllowTTL - time.Duration(1), + DenyTTL: minDenyTTL - time.Duration(1), + RetryBackoff: minRetryBackoff - time.Duration(1), + }, + wantErr: true, + }, + { + test: "config above normal ranges, error", + config: imagePolicyWebhookConfig{ + AllowTTL: time.Duration(1) + maxAllowTTL, + DenyTTL: time.Duration(1) + maxDenyTTL, + RetryBackoff: time.Duration(1) + maxRetryBackoff, + }, + wantErr: true, + }, + { + test: "config wants default values", + config: imagePolicyWebhookConfig{ + AllowTTL: useDefault, + DenyTTL: useDefault, + RetryBackoff: useDefault, + }, + normalizedConfig: imagePolicyWebhookConfig{ + AllowTTL: defaultAllowTTL, + DenyTTL: defaultDenyTTL, + RetryBackoff: defaultRetryBackoff, + }, + wantErr: false, + }, + { + test: "config wants disabled values", + config: imagePolicyWebhookConfig{ + AllowTTL: disableTTL, + DenyTTL: disableTTL, + RetryBackoff: disableTTL, + }, + normalizedConfig: imagePolicyWebhookConfig{ + AllowTTL: time.Duration(0), + DenyTTL: time.Duration(0), + RetryBackoff: time.Duration(0), + }, + wantErr: false, + }, + } + for _, tt := range tests { + err := normalizeWebhookConfig(&tt.config) + if err == nil && tt.wantErr == true { + t.Errorf("%s: expected error from normalization and didn't have one", tt.test) + } + if err != nil && tt.wantErr == false { + t.Errorf("%s: unexpected error from normalization: %v", tt.test, err) + } + if err == nil && !reflect.DeepEqual(tt.config, tt.normalizedConfig) { + t.Errorf("%s: expected config to be normalized. got: %v expected: %v", tt.test, tt.config, tt.normalizedConfig) + } + } +} diff --git a/plugin/pkg/admission/imagepolicy/doc.go b/plugin/pkg/admission/imagepolicy/doc.go new file mode 100644 index 00000000000..508ff33b17c --- /dev/null +++ b/plugin/pkg/admission/imagepolicy/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2016 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. +*/ + +// Package imagepolicy checks a webhook for image admission +package imagepolicy // import "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy" diff --git a/plugin/pkg/admission/imagepolicy/gencerts.sh b/plugin/pkg/admission/imagepolicy/gencerts.sh new file mode 100755 index 00000000000..384e8f4e9d1 --- /dev/null +++ b/plugin/pkg/admission/imagepolicy/gencerts.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# Copyright 2016 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. + +set -e + +# gencerts.sh generates the certificates for the webhook authz plugin tests. +# +# It is not expected to be run often (there is no go generate rule), and mainly +# exists for documentation purposes. + +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 = serverAuth +subjectAltName = @alt_names +[alt_names] +IP.1 = 127.0.0.1 +EOF + +cat > client.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 +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=webhook_imagepolicy_ca" + +# Create a second certificate authority +openssl genrsa -out badCAKey.pem 2048 +openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=webhook_imagepolicy_ca" + +# Create a server certiticate +openssl genrsa -out serverKey.pem 2048 +openssl req -new -key serverKey.pem -out server.csr -subj "/CN=webhook_imagepolicy_server" -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 + +# Create a client certiticate +openssl genrsa -out clientKey.pem 2048 +openssl req -new -key clientKey.pem -out client.csr -subj "/CN=webhook_imagepolicy_client" -config client.conf +openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf + +outfile=certs_test.go + +cat > $outfile << EOF +/* +Copyright 2016 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. +*/ + +EOF + +echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile +echo "// and holds raw certificates for the imagepolicy webhook tests." >> $outfile +echo "" >> $outfile +echo "package imagepolicy" >> $outfile +for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; 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