/*
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 (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"math/rand"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strconv"
	"testing"
	"time"

	"k8s.io/api/imagepolicy/v1alpha1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apiserver/pkg/admission"
	"k8s.io/apiserver/pkg/authentication/user"
	v1 "k8s.io/client-go/tools/clientcmd/api/v1"
	api "k8s.io/kubernetes/pkg/apis/core"

	"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: true,
		},
		{
			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: true,
		},
		{
			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.
		t.Run(tt.msg, func(t *testing.T) {
			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)
				}
				defer configFile.Close()

				_, err = NewImagePolicyWebhook(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"`
			AuditAnnotations map[string]string `json:"auditAnnotations"`
		}
		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,
				review.Status.AuditAnnotations,
			},
		}
		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
	outAnnotations map[string]string
}

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"
	}

	r.Status.AuditAnnotations = m.outAnnotations
}
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) (*Plugin, 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)
	}
	defer configFile.Close()
	wh, err := NewImagePolicyWebhook(configFile)
	if err != nil {
		return nil, err
	}
	return wh, 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.
		t.Run(tt.test, func(t *testing.T) {
			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, &metav1.CreateOptions{}, false, &user.DefaultInfo{})

			// Allow all and see if we get an error.
			service.Allow()

			err = wh.Validate(context.TODO(), attr, nil)
			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.Validate(context.TODO(), attr, nil); 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 *Plugin, attr admission.Attributes, tests []webhookCacheTestCase) {
	for _, test := range tests {
		serv.statusCode = test.statusCode
		err := wh.Validate(context.TODO(), attr, nil)
		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, &metav1.CreateOptions{}, false, &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, &metav1.CreateOptions{}, false, &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.
		t.Run(tt.test, func(t *testing.T) {
			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, &metav1.CreateOptions{}, false, &user.DefaultInfo{})

			err = wh.Validate(context.TODO(), attr, nil)
			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
			}
		})
	}
}

// fakeAttributes decorate kadmission.Attributes. It's used to trace the added annotations.
type fakeAttributes struct {
	admission.Attributes
	annotations map[string]string
}

func (f fakeAttributes) AddAnnotation(k, v string) error {
	f.annotations[k] = v
	return f.Attributes.AddAnnotation(k, v)
}

func TestDefaultAllow(t *testing.T) {
	tests := []struct {
		test                               string
		pod                                *api.Pod
		defaultAllow                       bool
		wantAllowed, wantErr, wantFailOpen bool
	}{
		{
			test:         "DefaultAllow = true, backend unreachable, bad image",
			pod:          goodPod("bad"),
			defaultAllow: true,
			wantAllowed:  true,
			wantFailOpen: true,
		},
		{
			test:         "DefaultAllow = true, backend unreachable, good image",
			pod:          goodPod("good"),
			defaultAllow: true,
			wantAllowed:  true,
			wantFailOpen: true,
		},
		{
			test:         "DefaultAllow = false, backend unreachable, good image",
			pod:          goodPod("good"),
			defaultAllow: false,
			wantAllowed:  false,
			wantErr:      true,
			wantFailOpen: false,
		},
		{
			test:         "DefaultAllow = false, backend unreachable, bad image",
			pod:          goodPod("bad"),
			defaultAllow: false,
			wantAllowed:  false,
			wantErr:      true,
			wantFailOpen: false,
		},
	}
	for _, tt := range tests {
		// Use a closure so defer statements trigger between loop iterations.
		t.Run(tt.test, func(t *testing.T) {
			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, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
			annotations := make(map[string]string)
			attr = &fakeAttributes{attr, annotations}

			err = wh.Validate(context.TODO(), attr, nil)
			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
			}
			podAnnotations := tt.pod.GetAnnotations()
			if tt.wantFailOpen {
				if podAnnotations == nil || podAnnotations[api.ImagePolicyFailedOpenKey] != "true" {
					t.Errorf("missing expected fail open pod annotation")
				}
				if annotations[AuditKeyPrefix+ImagePolicyFailedOpenKeySuffix] != "true" {
					t.Errorf("missing expected fail open attributes annotation")
				}
			} else {
				if podAnnotations != nil && podAnnotations[api.ImagePolicyFailedOpenKey] == "true" {
					t.Errorf("found unexpected fail open pod annotation")
				}
				if annotations[AuditKeyPrefix+ImagePolicyFailedOpenKeySuffix] == "true" {
					t.Errorf("found unexpected fail open attributes annotation")
				}
			}
		})
	}
}

// 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.
		t.Run(tt.test, func(t *testing.T) {
			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, &metav1.CreateOptions{}, false, &user.DefaultInfo{})

			err = wh.Validate(context.TODO(), attr, nil)
			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 TestReturnedAnnotationAdd(t *testing.T) {
	tests := []struct {
		test                string
		pod                 *api.Pod
		verifierAnnotations map[string]string
		expectedAnnotations map[string]string
		wantErr             bool
	}{
		{
			test: "Add valid response annotations",
			pod:  goodPod("good"),
			verifierAnnotations: map[string]string{
				"foo-test": "true",
				"bar-test": "false",
			},
			expectedAnnotations: map[string]string{
				"imagepolicywebhook.image-policy.k8s.io/foo-test": "true",
				"imagepolicywebhook.image-policy.k8s.io/bar-test": "false",
			},
		},
		{
			test:                "No returned annotations are ignored",
			pod:                 goodPod("good"),
			verifierAnnotations: map[string]string{},
			expectedAnnotations: map[string]string{},
		},
		{
			test:                "Handles nil annotations",
			pod:                 goodPod("good"),
			verifierAnnotations: nil,
			expectedAnnotations: map[string]string{},
		},
		{
			test: "Adds annotations for bad request",
			pod: &api.Pod{
				Spec: api.PodSpec{
					ServiceAccountName: "default",
					SecurityContext:    &api.PodSecurityContext{},
					Containers: []api.Container{
						{
							Image:           "bad",
							SecurityContext: &api.SecurityContext{},
						},
					},
				},
			},
			verifierAnnotations: map[string]string{
				"foo-test": "false",
			},
			expectedAnnotations: map[string]string{
				"imagepolicywebhook.image-policy.k8s.io/foo-test": "false",
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		// Use a closure so defer statements trigger between loop iterations.
		t.Run(tt.test, func(t *testing.T) {
			service := new(mockService)
			service.statusCode = 200
			service.outAnnotations = tt.verifierAnnotations

			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 := tt.pod

			attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
			annotations := make(map[string]string)
			attr = &fakeAttributes{attr, annotations}

			err = wh.Validate(context.TODO(), attr, nil)
			if tt.wantErr {
				if err == nil {
					t.Errorf("%s: expected error making admission request: %v", tt.test, err)
				}
			} else if err != nil {
				t.Errorf("%s: failed to admit: %v", tt.test, err)
			}

			if !reflect.DeepEqual(annotations, tt.expectedAnnotations) {
				t.Errorf("got audit annotations: %v; want: %v", annotations, tt.expectedAnnotations)
			}
		})
	}
}

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{},
				},
			},
		},
	}
}
