vendor files

This commit is contained in:
Serguei Bezverkhi
2018-01-09 13:57:14 -05:00
parent 558bc6c02a
commit 7b24313bd6
16547 changed files with 4527373 additions and 0 deletions

View File

@ -0,0 +1,63 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"config.go",
"doc.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy",
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/apis/imagepolicy/install:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/imagepolicy/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/cache:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/webhook:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"certs_test.go",
"config_test.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/imagepolicy/install:go_default_library",
"//vendor/k8s.io/api/imagepolicy/v1alpha1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,251 @@
/*
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/api/imagepolicy/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/cache"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
// install the clientgo image policy API for use with api registry
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
)
var (
groupVersions = []schema.GroupVersion{v1alpha1.SchemeGroupVersion}
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("ImagePolicyWebhook", func(config io.Reader) (admission.Interface, error) {
newImagePolicyWebhook, err := NewImagePolicyWebhook(config)
if err != nil {
return nil, err
}
return newImagePolicyWebhook, nil
})
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*admission.Handler
webhook *webhook.GenericWebhook
responseCache *cache.LRUExpireCache
allowTTL time.Duration
denyTTL time.Duration
retryBackoff time.Duration
defaultAllow bool
}
var _ admission.ValidationInterface = &Plugin{}
func (a *Plugin) 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 *Plugin) 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 *Plugin) webhookError(pod *api.Pod, attributes admission.Attributes, err error) error {
if err != nil {
glog.V(2).Infof("error contacting webhook backend: %s", err)
if a.defaultAllow {
annotations := pod.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[api.ImagePolicyFailedOpenKey] = "true"
pod.ObjectMeta.SetAnnotations(annotations)
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
}
// Validate makes an admission decision based on the request attributes
func (a *Plugin) Validate(attributes admission.Attributes) (err error) {
// Ignore all calls to subresources or resources other than pods.
if attributes.GetSubresource() != "" || attributes.GetResource().GroupResource() != api.Resource("pods") {
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(pod, attributes, &imageReview); err != nil {
return admission.NewForbidden(attributes, err)
}
return nil
}
func (a *Plugin) admitPod(pod *api.Pod, 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() rest.Result {
return a.webhook.RestClient.Post().Body(review).Do()
})
if err := result.Error(); err != nil {
return a.webhookError(pod, attributes, err)
}
var statusCode int
if result.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
return a.webhookError(pod, attributes, fmt.Errorf("Error contacting webhook: %d", statusCode))
}
if err := result.Into(review); err != nil {
return a.webhookError(pod, 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 webhook 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 plugin from the provided config file.
// The config file is specified by --admission-control-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(configFile io.Reader) (*Plugin, error) {
if configFile == nil {
return nil, fmt.Errorf("no config specified")
}
// TODO: move this to a versioned configuration file format
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(legacyscheme.Registry, legacyscheme.Codecs, whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff)
if err != nil {
return nil, err
}
return &Plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
webhook: gw,
responseCache: cache.NewLRUExpireCache(1024),
allowTTL: whConfig.AllowTTL,
denyTTL: whConfig.DenyTTL,
defaultAllow: whConfig.DefaultAllow,
}, nil
}

View File

@ -0,0 +1,948 @@
/*
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"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
"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.
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"`
}
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) (*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.
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.Validate(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.Validate(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 *Plugin, attr admission.Attributes, tests []webhookCacheTestCase) {
for _, test := range tests {
serv.statusCode = test.statusCode
err := wh.Validate(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.Validate(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.Validate(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.Validate(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{},
},
},
},
}
}

View File

@ -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-----`)

View File

@ -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 default 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
}

View File

@ -0,0 +1,133 @@
/*
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,
},
{
test: "config within normal ranges for min values",
config: imagePolicyWebhookConfig{
AllowTTL: minAllowTTL / time.Second,
DenyTTL: minDenyTTL / time.Second,
RetryBackoff: minRetryBackoff,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: minAllowTTL,
DenyTTL: minDenyTTL,
RetryBackoff: minRetryBackoff * time.Millisecond,
},
wantErr: false,
},
{
test: "config within normal ranges for max values",
config: imagePolicyWebhookConfig{
AllowTTL: maxAllowTTL / time.Second,
DenyTTL: maxDenyTTL / time.Second,
RetryBackoff: maxRetryBackoff / time.Millisecond,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: maxAllowTTL,
DenyTTL: maxDenyTTL,
RetryBackoff: maxRetryBackoff,
},
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)
}
}
}

View File

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

View File

@ -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