mirror of https://github.com/k3s-io/k3s
1081 lines
29 KiB
Go
1081 lines
29 KiB
Go
/*
|
|
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{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|