1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081 |
- /*
- 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/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(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(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(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(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(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(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
- }{
- {
- 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",
- },
- },
- }
- 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(attr, nil)
- 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{},
- },
- },
- },
- }
- }
|