mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 15:25:57 +00:00
Merge pull request #30631 from ecordell/webhook-admission
Automatic merge from submit-queue ImagePolicyWebhook Admission Controller <!-- Thanks for sending a pull request! Here are some tips for you: 1. If this is your first time, read our contributor guidelines https://github.com/kubernetes/kubernetes/blob/master/CONTRIBUTING.md and developer guide https://github.com/kubernetes/kubernetes/blob/master/docs/devel/development.md 2. If you want *faster* PR reviews, read how: https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md 3. Follow the instructions for writing a release note: https://github.com/kubernetes/kubernetes/blob/master/docs/devel/pull-requests.md#release-notes --> **What this PR does / why we need it**: This is an implementation of the [image provenance proposal](https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/image-provenance.md). It also includes the API definitions by @Q-Lee from https://github.com/kubernetes/kubernetes/pull/30241 **Special notes for your reviewer**: Please note that this is the first admission controller to make use of the admission controller config file (`--admission-controller-config-file`). I have defined a format for it but we may want to double check it's adequate for future use cases as well. The format defined is: ``` { "imagePolicy": { "kubeConfigFile": "path/to/kubeconfig/for/backend", "allowTTL": 50, # time in s to cache approval "denyTTL": 50, # time in s to cache denial "retryBackoff": 500, # time in ms to wait between retries "defaultAllow": true # determines behavior if the webhook backend fails } } ``` (or yaml) **Release note**: <!-- Steps to write your release note: 1. Use the release-note-* labels to set the release note state (if you have access) 2. Enter your extended release note in the below block; leaving it blank means using the PR title as the release note. If no release note is required, just write `NONE`. --> ```release-note Adding ImagePolicyWebhook admission controller. ```
This commit is contained in:
commit
1de78d5a90
@ -169,6 +169,7 @@ plugin/pkg/admission/admit
|
|||||||
plugin/pkg/admission/alwayspullimages
|
plugin/pkg/admission/alwayspullimages
|
||||||
plugin/pkg/admission/deny
|
plugin/pkg/admission/deny
|
||||||
plugin/pkg/admission/exec
|
plugin/pkg/admission/exec
|
||||||
|
plugin/pkg/admission/imagepolicy
|
||||||
plugin/pkg/admission/namespace/autoprovision
|
plugin/pkg/admission/namespace/autoprovision
|
||||||
plugin/pkg/admission/namespace/exists
|
plugin/pkg/admission/namespace/exists
|
||||||
plugin/pkg/admission/securitycontext/scdeny
|
plugin/pkg/admission/securitycontext/scdeny
|
||||||
@ -202,4 +203,4 @@ pkg/util/maps
|
|||||||
pkg/volume/quobyte
|
pkg/volume/quobyte
|
||||||
test/integration/discoverysummarizer
|
test/integration/discoverysummarizer
|
||||||
test/integration/examples
|
test/integration/examples
|
||||||
test/integration/federation
|
test/integration/federation
|
239
plugin/pkg/admission/imagepolicy/admission.go
Normal file
239
plugin/pkg/admission/imagepolicy/admission.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package imagepolicy contains an admission controller that configures a webhook to which policy
|
||||||
|
// decisions are delegated.
|
||||||
|
package imagepolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
apierrors "k8s.io/kubernetes/pkg/api/errors"
|
||||||
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/imagepolicy/v1alpha1"
|
||||||
|
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||||
|
"k8s.io/kubernetes/pkg/util/yaml"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/client/restclient"
|
||||||
|
"k8s.io/kubernetes/pkg/util/cache"
|
||||||
|
"k8s.io/kubernetes/plugin/pkg/webhook"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/admission"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
groupVersions = []unversioned.GroupVersion{v1alpha1.SchemeGroupVersion}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
admission.RegisterPlugin("ImagePolicyWebhook", func(client clientset.Interface, config io.Reader) (admission.Interface, error) {
|
||||||
|
newImagePolicyWebhook, err := NewImagePolicyWebhook(client, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newImagePolicyWebhook, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// imagePolicyWebhook is an implementation of admission.Interface.
|
||||||
|
type imagePolicyWebhook struct {
|
||||||
|
*admission.Handler
|
||||||
|
webhook *webhook.GenericWebhook
|
||||||
|
responseCache *cache.LRUExpireCache
|
||||||
|
allowTTL time.Duration
|
||||||
|
denyTTL time.Duration
|
||||||
|
retryBackoff time.Duration
|
||||||
|
defaultAllow bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *imagePolicyWebhook) statusTTL(status v1alpha1.ImageReviewStatus) time.Duration {
|
||||||
|
if status.Allowed {
|
||||||
|
return a.allowTTL
|
||||||
|
}
|
||||||
|
return a.denyTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out annotations that don't match *.image-policy.k8s.io/*
|
||||||
|
func (a *imagePolicyWebhook) filterAnnotations(allAnnotations map[string]string) map[string]string {
|
||||||
|
annotations := make(map[string]string)
|
||||||
|
for k, v := range allAnnotations {
|
||||||
|
if strings.Contains(k, ".image-policy.k8s.io/") {
|
||||||
|
annotations[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to call on webhook failure; behavior determined by defaultAllow flag
|
||||||
|
func (a *imagePolicyWebhook) webhookError(attributes admission.Attributes, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
glog.V(2).Infof("error contacting webhook backend: %s")
|
||||||
|
if a.defaultAllow {
|
||||||
|
glog.V(2).Infof("resource allowed in spite of webhook backend failure")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
glog.V(2).Infof("resource not allowed due to webhook backend failure ")
|
||||||
|
return admission.NewForbidden(attributes, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *imagePolicyWebhook) Admit(attributes admission.Attributes) (err error) {
|
||||||
|
// Ignore all calls to subresources or resources other than pods.
|
||||||
|
allowedResources := map[unversioned.GroupResource]bool{
|
||||||
|
api.Resource("pods"): true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(attributes.GetSubresource()) != 0 || !allowedResources[attributes.GetResource().GroupResource()] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pod, ok := attributes.GetObject().(*api.Pod)
|
||||||
|
if !ok {
|
||||||
|
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build list of ImageReviewContainerSpec
|
||||||
|
var imageReviewContainerSpecs []v1alpha1.ImageReviewContainerSpec
|
||||||
|
containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
|
||||||
|
containers = append(containers, pod.Spec.Containers...)
|
||||||
|
containers = append(containers, pod.Spec.InitContainers...)
|
||||||
|
for _, c := range containers {
|
||||||
|
imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{
|
||||||
|
Image: c.Image,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
imageReview := v1alpha1.ImageReview{
|
||||||
|
Spec: v1alpha1.ImageReviewSpec{
|
||||||
|
Containers: imageReviewContainerSpecs,
|
||||||
|
Annotations: a.filterAnnotations(pod.Annotations),
|
||||||
|
Namespace: attributes.GetNamespace(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := a.admitPod(attributes, &imageReview); err != nil {
|
||||||
|
return admission.NewForbidden(attributes, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *imagePolicyWebhook) admitPod(attributes admission.Attributes, review *v1alpha1.ImageReview) error {
|
||||||
|
cacheKey, err := json.Marshal(review.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if entry, ok := a.responseCache.Get(string(cacheKey)); ok {
|
||||||
|
review.Status = entry.(v1alpha1.ImageReviewStatus)
|
||||||
|
} else {
|
||||||
|
result := a.webhook.WithExponentialBackoff(func() restclient.Result {
|
||||||
|
return a.webhook.RestClient.Post().Body(review).Do()
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := result.Error(); err != nil {
|
||||||
|
return a.webhookError(attributes, err)
|
||||||
|
}
|
||||||
|
var statusCode int
|
||||||
|
if result.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
|
||||||
|
return a.webhookError(attributes, fmt.Errorf("Error contacting webhook: %d", statusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := result.Into(review); err != nil {
|
||||||
|
return a.webhookError(attributes, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.responseCache.Add(string(cacheKey), review.Status, a.statusTTL(review.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !review.Status.Allowed {
|
||||||
|
if len(review.Status.Reason) > 0 {
|
||||||
|
return fmt.Errorf("image policy webook backend denied one or more images: %s", review.Status.Reason)
|
||||||
|
}
|
||||||
|
return errors.New("one or more images rejected by webhook backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewImagePolicyWebhook a new imagePolicyWebhook from the provided config file.
|
||||||
|
// The config file is specified by --admission-controller-config-file and has the
|
||||||
|
// following format for a webhook:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "imagePolicy": {
|
||||||
|
// "kubeConfigFile": "path/to/kubeconfig/for/backend",
|
||||||
|
// "allowTTL": 30, # time in s to cache approval
|
||||||
|
// "denyTTL": 30, # time in s to cache denial
|
||||||
|
// "retryBackoff": 500, # time in ms to wait between retries
|
||||||
|
// "defaultAllow": true # determines behavior if the webhook backend fails
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The config file may be json or yaml.
|
||||||
|
//
|
||||||
|
// The kubeconfig property refers to another file in the kubeconfig format which
|
||||||
|
// specifies how to connect to the webhook backend.
|
||||||
|
//
|
||||||
|
// The kubeconfig's cluster field is used to refer to the remote service, user refers to the returned authorizer.
|
||||||
|
//
|
||||||
|
// # clusters refers to the remote service.
|
||||||
|
// clusters:
|
||||||
|
// - name: name-of-remote-imagepolicy-service
|
||||||
|
// cluster:
|
||||||
|
// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
|
||||||
|
// server: https://images.example.com/policy # URL of remote service to query. Must use 'https'.
|
||||||
|
//
|
||||||
|
// # users refers to the API server's webhook configuration.
|
||||||
|
// users:
|
||||||
|
// - name: name-of-api-server
|
||||||
|
// user:
|
||||||
|
// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
|
||||||
|
// client-key: /path/to/key.pem # key matching the cert
|
||||||
|
//
|
||||||
|
// For additional HTTP configuration, refer to the kubeconfig documentation
|
||||||
|
// http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html.
|
||||||
|
func NewImagePolicyWebhook(client clientset.Interface, configFile io.Reader) (admission.Interface, error) {
|
||||||
|
var config AdmissionConfig
|
||||||
|
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
|
||||||
|
err := d.Decode(&config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
whConfig := config.ImagePolicyWebhook
|
||||||
|
if err := normalizeWebhookConfig(&whConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gw, err := webhook.NewGenericWebhook(whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &imagePolicyWebhook{
|
||||||
|
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||||
|
webhook: gw,
|
||||||
|
responseCache: cache.NewLRUExpireCache(1024),
|
||||||
|
allowTTL: whConfig.AllowTTL,
|
||||||
|
denyTTL: whConfig.DenyTTL,
|
||||||
|
defaultAllow: whConfig.DefaultAllow,
|
||||||
|
}, nil
|
||||||
|
}
|
944
plugin/pkg/admission/imagepolicy/admission_test.go
Normal file
944
plugin/pkg/admission/imagepolicy/admission_test.go
Normal file
@ -0,0 +1,944 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package imagepolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/admission"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/imagepolicy/v1alpha1"
|
||||||
|
"k8s.io/kubernetes/pkg/auth/user"
|
||||||
|
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
|
||||||
|
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api/v1"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultConfigTmplJSON = `
|
||||||
|
{
|
||||||
|
"imagePolicy": {
|
||||||
|
"kubeConfigFile": "{{ .KubeConfig }}",
|
||||||
|
"allowTTL": {{ .AllowTTL }},
|
||||||
|
"denyTTL": {{ .DenyTTL }},
|
||||||
|
"retryBackoff": {{ .RetryBackoff }},
|
||||||
|
"defaultAllow": {{ .DefaultAllow }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const defaultConfigTmplYAML = `
|
||||||
|
imagePolicy:
|
||||||
|
kubeConfigFile: "{{ .KubeConfig }}"
|
||||||
|
allowTTL: {{ .AllowTTL }}
|
||||||
|
denyTTL: {{ .DenyTTL }}
|
||||||
|
retryBackoff: {{ .RetryBackoff }}
|
||||||
|
defaultAllow: {{ .DefaultAllow }}
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestNewFromConfig(t *testing.T) {
|
||||||
|
dir, err := ioutil.TempDir("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
CA string
|
||||||
|
Cert string
|
||||||
|
Key string
|
||||||
|
}{
|
||||||
|
CA: filepath.Join(dir, "ca.pem"),
|
||||||
|
Cert: filepath.Join(dir, "clientcert.pem"),
|
||||||
|
Key: filepath.Join(dir, "clientkey.pem"),
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
}{
|
||||||
|
{data.CA, caCert},
|
||||||
|
{data.Cert, clientCert},
|
||||||
|
{data.Key, clientKey},
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
msg string
|
||||||
|
kubeConfigTmpl string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
msg: "a single cluster and single user",
|
||||||
|
kubeConfigTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://admission.example.com
|
||||||
|
name: foobar
|
||||||
|
users:
|
||||||
|
- name: a cluster
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
msg: "multiple clusters with no context",
|
||||||
|
kubeConfigTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://admission.example.com
|
||||||
|
name: foobar
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: a bad certificate path
|
||||||
|
server: https://admission.example.com
|
||||||
|
name: barfoo
|
||||||
|
users:
|
||||||
|
- name: a name
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
msg: "multiple clusters with a context",
|
||||||
|
kubeConfigTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: a bad certificate path
|
||||||
|
server: https://admission.example.com
|
||||||
|
name: foobar
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://admission.example.com
|
||||||
|
name: barfoo
|
||||||
|
users:
|
||||||
|
- name: a name
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
contexts:
|
||||||
|
- name: default
|
||||||
|
context:
|
||||||
|
cluster: barfoo
|
||||||
|
user: a name
|
||||||
|
current-context: default
|
||||||
|
`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
msg: "cluster with bad certificate path specified",
|
||||||
|
kubeConfigTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: a bad certificate path
|
||||||
|
server: https://admission.example.com
|
||||||
|
name: foobar
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://admission.example.com
|
||||||
|
name: barfoo
|
||||||
|
users:
|
||||||
|
- name: a name
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
contexts:
|
||||||
|
- name: default
|
||||||
|
context:
|
||||||
|
cluster: foobar
|
||||||
|
user: a name
|
||||||
|
current-context: default
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
err := func() error {
|
||||||
|
tempfile, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p := tempfile.Name()
|
||||||
|
defer os.Remove(p)
|
||||||
|
|
||||||
|
tmpl, err := template.New("test").Parse(tt.kubeConfigTmpl)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse test template: %v", err)
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(tempfile, data); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute test template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempconfigfile, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pc := tempconfigfile.Name()
|
||||||
|
defer os.Remove(pc)
|
||||||
|
|
||||||
|
configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplJSON)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse test template: %v", err)
|
||||||
|
}
|
||||||
|
dataConfig := struct {
|
||||||
|
KubeConfig string
|
||||||
|
AllowTTL int
|
||||||
|
DenyTTL int
|
||||||
|
RetryBackoff int
|
||||||
|
DefaultAllow bool
|
||||||
|
}{
|
||||||
|
KubeConfig: p,
|
||||||
|
AllowTTL: 500,
|
||||||
|
DenyTTL: 500,
|
||||||
|
RetryBackoff: 500,
|
||||||
|
DefaultAllow: true,
|
||||||
|
}
|
||||||
|
if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute test template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new admission controller
|
||||||
|
configFile, err := os.Open(pc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = NewImagePolicyWebhook(fake.NewSimpleClientset(), configFile)
|
||||||
|
return err
|
||||||
|
}()
|
||||||
|
if err != nil && !tt.wantErr {
|
||||||
|
t.Errorf("failed to load plugin from config %q: %v", tt.msg, err)
|
||||||
|
}
|
||||||
|
if err == nil && tt.wantErr {
|
||||||
|
t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service mocks a remote service.
|
||||||
|
type Service interface {
|
||||||
|
Review(*v1alpha1.ImageReview)
|
||||||
|
HTTPStatusCode() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestServer wraps a Service as an httptest.Server.
|
||||||
|
func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
if cert != nil {
|
||||||
|
cert, err := tls.X509KeyPair(cert, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if caCert != nil {
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
rootCAs.AppendCertsFromPEM(caCert)
|
||||||
|
if tlsConfig == nil {
|
||||||
|
tlsConfig = &tls.Config{}
|
||||||
|
}
|
||||||
|
tlsConfig.ClientCAs = rootCAs
|
||||||
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
}
|
||||||
|
|
||||||
|
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var review v1alpha1.ImageReview
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
|
||||||
|
http.Error(w, "HTTP Error", s.HTTPStatusCode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Review(&review)
|
||||||
|
type status struct {
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
resp := struct {
|
||||||
|
APIVersion string `json:"apiVersion"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Status status `json:"status"`
|
||||||
|
}{
|
||||||
|
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||||
|
Kind: "ImageReview",
|
||||||
|
Status: status{review.Status.Allowed, review.Status.Reason},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
|
||||||
|
server.TLS = tlsConfig
|
||||||
|
server.StartTLS()
|
||||||
|
return server, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A service that can be set to allow all or deny all authorization requests.
|
||||||
|
type mockService struct {
|
||||||
|
allow bool
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockService) Review(r *v1alpha1.ImageReview) {
|
||||||
|
r.Status.Allowed = m.allow
|
||||||
|
|
||||||
|
// hardcoded overrides
|
||||||
|
if r.Spec.Containers[0].Image == "good" {
|
||||||
|
r.Status.Allowed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range r.Spec.Containers {
|
||||||
|
if c.Image == "bad" {
|
||||||
|
r.Status.Allowed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Status.Allowed {
|
||||||
|
r.Status.Reason = "not allowed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *mockService) Allow() { m.allow = true }
|
||||||
|
func (m *mockService) Deny() { m.allow = false }
|
||||||
|
func (m *mockService) HTTPStatusCode() int { return m.statusCode }
|
||||||
|
|
||||||
|
// newImagePolicyWebhook creates a temporary kubeconfig file from the provided arguments and attempts to load
|
||||||
|
// a new newImagePolicyWebhook from it.
|
||||||
|
func newImagePolicyWebhook(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, defaultAllow bool) (*imagePolicyWebhook, error) {
|
||||||
|
tempfile, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := tempfile.Name()
|
||||||
|
defer os.Remove(p)
|
||||||
|
config := v1.Config{
|
||||||
|
Clusters: []v1.NamedCluster{
|
||||||
|
{
|
||||||
|
Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthInfos: []v1.NamedAuthInfo{
|
||||||
|
{
|
||||||
|
AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tempconfigfile, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pc := tempconfigfile.Name()
|
||||||
|
defer os.Remove(pc)
|
||||||
|
|
||||||
|
configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplYAML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse test template: %v", err)
|
||||||
|
}
|
||||||
|
dataConfig := struct {
|
||||||
|
KubeConfig string
|
||||||
|
AllowTTL int64
|
||||||
|
DenyTTL int64
|
||||||
|
RetryBackoff int64
|
||||||
|
DefaultAllow bool
|
||||||
|
}{
|
||||||
|
KubeConfig: p,
|
||||||
|
AllowTTL: cacheTime.Nanoseconds(),
|
||||||
|
DenyTTL: cacheTime.Nanoseconds(),
|
||||||
|
RetryBackoff: 0,
|
||||||
|
DefaultAllow: defaultAllow,
|
||||||
|
}
|
||||||
|
if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute test template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new admission controller
|
||||||
|
configFile, err := os.Open(pc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read test config: %v", err)
|
||||||
|
}
|
||||||
|
wh, err := NewImagePolicyWebhook(fake.NewSimpleClientset(), configFile)
|
||||||
|
return wh.(*imagePolicyWebhook), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTLSConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
clientCert, clientKey, clientCA []byte
|
||||||
|
serverCert, serverKey, serverCA []byte
|
||||||
|
wantAllowed, wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "TLS setup between client and server",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
|
||||||
|
wantAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not require client auth",
|
||||||
|
clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
wantAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not require client auth, client provides it",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
wantAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Client does not trust server",
|
||||||
|
clientCert: clientCert, clientKey: clientKey,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not trust client",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Plugin does not support insecure configurations.
|
||||||
|
test: "Server is using insecure connection",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
func() {
|
||||||
|
service := new(mockService)
|
||||||
|
service.statusCode = 200
|
||||||
|
|
||||||
|
server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wh, err := newImagePolicyWebhook(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, -1, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pod := goodPod(strconv.Itoa(rand.Intn(1000)))
|
||||||
|
attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
|
||||||
|
|
||||||
|
// Allow all and see if we get an error.
|
||||||
|
service.Allow()
|
||||||
|
|
||||||
|
err = wh.Admit(attr)
|
||||||
|
if tt.wantAllowed {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected successful admission")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected failed admission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error making admission request: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to admit with AllowAll policy: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Deny()
|
||||||
|
if err := wh.Admit(attr); err == nil {
|
||||||
|
t.Errorf("%s: incorrectly admitted with DenyAll policy", tt.test)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type webhookCacheTestCase struct {
|
||||||
|
statusCode int
|
||||||
|
expectedErr bool
|
||||||
|
expectedAuthorized bool
|
||||||
|
expectedCached bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWebhookCacheCases(t *testing.T, serv *mockService, wh *imagePolicyWebhook, attr admission.Attributes, tests []webhookCacheTestCase) {
|
||||||
|
for _, test := range tests {
|
||||||
|
serv.statusCode = test.statusCode
|
||||||
|
err := wh.Admit(attr)
|
||||||
|
authorized := err == nil
|
||||||
|
|
||||||
|
if test.expectedErr && err == nil {
|
||||||
|
t.Errorf("Expected error")
|
||||||
|
} else if !test.expectedErr && err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if test.expectedAuthorized && !authorized {
|
||||||
|
if test.expectedCached {
|
||||||
|
t.Errorf("Webhook should have successful response cached, but authorizer reported unauthorized.")
|
||||||
|
} else {
|
||||||
|
t.Errorf("Webhook returned HTTP %d, but authorizer reported unauthorized.", test.statusCode)
|
||||||
|
}
|
||||||
|
} else if !test.expectedAuthorized && authorized {
|
||||||
|
t.Errorf("Webhook returned HTTP %d, but authorizer reported success.", test.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWebhookCache verifies that error responses from the server are not
|
||||||
|
// cached, but successful responses are.
|
||||||
|
func TestWebhookCache(t *testing.T) {
|
||||||
|
serv := new(mockService)
|
||||||
|
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Create an admission controller that caches successful responses.
|
||||||
|
wh, err := newImagePolicyWebhook(s.URL, clientCert, clientKey, caCert, 200, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []webhookCacheTestCase{
|
||||||
|
{statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false},
|
||||||
|
{statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCached: false},
|
||||||
|
{statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCached: false},
|
||||||
|
{statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCached: false},
|
||||||
|
{statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false},
|
||||||
|
{statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := admission.NewAttributesRecord(goodPod("test"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
|
||||||
|
|
||||||
|
serv.allow = true
|
||||||
|
|
||||||
|
testWebhookCacheCases(t, serv, wh, attr, tests)
|
||||||
|
|
||||||
|
// For a different request, webhook should be called again.
|
||||||
|
tests = []webhookCacheTestCase{
|
||||||
|
{statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false},
|
||||||
|
{statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false},
|
||||||
|
{statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true},
|
||||||
|
}
|
||||||
|
attr = admission.NewAttributesRecord(goodPod("test2"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
|
||||||
|
|
||||||
|
testWebhookCacheCases(t, serv, wh, attr, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerCombinations(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
pod *api.Pod
|
||||||
|
wantAllowed, wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "Single container allowed",
|
||||||
|
pod: goodPod("good"),
|
||||||
|
wantAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Single container denied",
|
||||||
|
pod: goodPod("bad"),
|
||||||
|
wantAllowed: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "One good container, one bad",
|
||||||
|
pod: &api.Pod{
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: "default",
|
||||||
|
SecurityContext: &api.PodSecurityContext{},
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "bad",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Image: "good",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAllowed: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Multiple good containers",
|
||||||
|
pod: &api.Pod{
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: "default",
|
||||||
|
SecurityContext: &api.PodSecurityContext{},
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "good",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Image: "good",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAllowed: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Multiple bad containers",
|
||||||
|
pod: &api.Pod{
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: "default",
|
||||||
|
SecurityContext: &api.PodSecurityContext{},
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "bad",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Image: "bad",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAllowed: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Good container, bad init container",
|
||||||
|
pod: &api.Pod{
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: "default",
|
||||||
|
SecurityContext: &api.PodSecurityContext{},
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "good",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitContainers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "bad",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAllowed: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Bad container, good init container",
|
||||||
|
pod: &api.Pod{
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: "default",
|
||||||
|
SecurityContext: &api.PodSecurityContext{},
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "bad",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitContainers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "good",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAllowed: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Good container, good init container",
|
||||||
|
pod: &api.Pod{
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: "default",
|
||||||
|
SecurityContext: &api.PodSecurityContext{},
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "good",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitContainers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: "good",
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAllowed: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
func() {
|
||||||
|
service := new(mockService)
|
||||||
|
service.statusCode = 200
|
||||||
|
|
||||||
|
server, err := NewTestServer(service, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
|
||||||
|
|
||||||
|
err = wh.Admit(attr)
|
||||||
|
if tt.wantAllowed {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected successful admission: %s", tt.test)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected failed admission: %s", tt.test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error making admission request: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to admit: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultAllow(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
pod *api.Pod
|
||||||
|
wantAllowed, wantErr, defaultAllow bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "DefaultAllow = true, backend unreachable, bad image",
|
||||||
|
pod: goodPod("bad"),
|
||||||
|
defaultAllow: true,
|
||||||
|
wantAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "DefaultAllow = true, backend unreachable, good image",
|
||||||
|
pod: goodPod("good"),
|
||||||
|
defaultAllow: true,
|
||||||
|
wantAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "DefaultAllow = false, backend unreachable, good image",
|
||||||
|
pod: goodPod("good"),
|
||||||
|
defaultAllow: false,
|
||||||
|
wantAllowed: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "DefaultAllow = false, backend unreachable, bad image",
|
||||||
|
pod: goodPod("bad"),
|
||||||
|
defaultAllow: false,
|
||||||
|
wantAllowed: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
func() {
|
||||||
|
service := new(mockService)
|
||||||
|
service.statusCode = 500
|
||||||
|
|
||||||
|
server, err := NewTestServer(service, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, tt.defaultAllow)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
|
||||||
|
|
||||||
|
err = wh.Admit(attr)
|
||||||
|
if tt.wantAllowed {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected successful admission")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected failed admission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error making admission request: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to admit: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A service that can record annotations sent to it
|
||||||
|
type annotationService struct {
|
||||||
|
annotations map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *annotationService) Review(r *v1alpha1.ImageReview) {
|
||||||
|
a.annotations = make(map[string]string)
|
||||||
|
for k, v := range r.Spec.Annotations {
|
||||||
|
a.annotations[k] = v
|
||||||
|
}
|
||||||
|
r.Status.Allowed = true
|
||||||
|
}
|
||||||
|
func (a *annotationService) HTTPStatusCode() int { return 200 }
|
||||||
|
func (a *annotationService) Annotations() map[string]string { return a.annotations }
|
||||||
|
|
||||||
|
func TestAnnotationFiltering(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
annotations map[string]string
|
||||||
|
outAnnotations map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "all annotations filtered out",
|
||||||
|
annotations: map[string]string{
|
||||||
|
"test": "test",
|
||||||
|
"another": "annotation",
|
||||||
|
"": "",
|
||||||
|
},
|
||||||
|
outAnnotations: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "image-policy annotations allowed",
|
||||||
|
annotations: map[string]string{
|
||||||
|
"my.image-policy.k8s.io/test": "test",
|
||||||
|
"other.image-policy.k8s.io/test2": "annotation",
|
||||||
|
"test": "test",
|
||||||
|
"another": "another",
|
||||||
|
"": "",
|
||||||
|
},
|
||||||
|
outAnnotations: map[string]string{
|
||||||
|
"my.image-policy.k8s.io/test": "test",
|
||||||
|
"other.image-policy.k8s.io/test2": "annotation",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
func() {
|
||||||
|
service := new(annotationService)
|
||||||
|
|
||||||
|
server, err := NewTestServer(service, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pod := goodPod("test")
|
||||||
|
pod.Annotations = tt.annotations
|
||||||
|
|
||||||
|
attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
|
||||||
|
|
||||||
|
err = wh.Admit(attr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected successful admission")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tt.outAnnotations, service.Annotations()) {
|
||||||
|
t.Errorf("expected annotations sent to webhook: %v to match expected: %v", service.Annotations(), tt.outAnnotations)
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func goodPod(containerID string) *api.Pod {
|
||||||
|
return &api.Pod{
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
ServiceAccountName: "default",
|
||||||
|
SecurityContext: &api.PodSecurityContext{},
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Image: containerID,
|
||||||
|
SecurityContext: &api.SecurityContext{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
211
plugin/pkg/admission/imagepolicy/certs_test.go
Normal file
211
plugin/pkg/admission/imagepolicy/certs_test.go
Normal 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-----`)
|
93
plugin/pkg/admission/imagepolicy/config.go
Normal file
93
plugin/pkg/admission/imagepolicy/config.go
Normal 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 defualt with 0 sentinel
|
||||||
|
if value == useDefault {
|
||||||
|
glog.V(2).Infof("image policy webhook %s using default value", name)
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to s; unmarshalling gives ns
|
||||||
|
value *= scale
|
||||||
|
|
||||||
|
// check value is within range
|
||||||
|
if value <= min || value > max {
|
||||||
|
return value, fmt.Errorf("valid value is between %v and %v, got %v", min, max, value)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
105
plugin/pkg/admission/imagepolicy/config_test.go
Normal file
105
plugin/pkg/admission/imagepolicy/config_test.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package imagepolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigNormalization(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
config imagePolicyWebhookConfig
|
||||||
|
normalizedConfig imagePolicyWebhookConfig
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "config within normal ranges",
|
||||||
|
config: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second,
|
||||||
|
DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second,
|
||||||
|
RetryBackoff: ((minRetryBackoff + maxRetryBackoff) / 2) / time.Millisecond,
|
||||||
|
},
|
||||||
|
normalizedConfig: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second * time.Second,
|
||||||
|
DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second * time.Second,
|
||||||
|
RetryBackoff: (minRetryBackoff + maxRetryBackoff) / 2,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "config below normal ranges, error",
|
||||||
|
config: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: minAllowTTL - time.Duration(1),
|
||||||
|
DenyTTL: minDenyTTL - time.Duration(1),
|
||||||
|
RetryBackoff: minRetryBackoff - time.Duration(1),
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "config above normal ranges, error",
|
||||||
|
config: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: time.Duration(1) + maxAllowTTL,
|
||||||
|
DenyTTL: time.Duration(1) + maxDenyTTL,
|
||||||
|
RetryBackoff: time.Duration(1) + maxRetryBackoff,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "config wants default values",
|
||||||
|
config: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: useDefault,
|
||||||
|
DenyTTL: useDefault,
|
||||||
|
RetryBackoff: useDefault,
|
||||||
|
},
|
||||||
|
normalizedConfig: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: defaultAllowTTL,
|
||||||
|
DenyTTL: defaultDenyTTL,
|
||||||
|
RetryBackoff: defaultRetryBackoff,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "config wants disabled values",
|
||||||
|
config: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: disableTTL,
|
||||||
|
DenyTTL: disableTTL,
|
||||||
|
RetryBackoff: disableTTL,
|
||||||
|
},
|
||||||
|
normalizedConfig: imagePolicyWebhookConfig{
|
||||||
|
AllowTTL: time.Duration(0),
|
||||||
|
DenyTTL: time.Duration(0),
|
||||||
|
RetryBackoff: time.Duration(0),
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
err := normalizeWebhookConfig(&tt.config)
|
||||||
|
if err == nil && tt.wantErr == true {
|
||||||
|
t.Errorf("%s: expected error from normalization and didn't have one", tt.test)
|
||||||
|
}
|
||||||
|
if err != nil && tt.wantErr == false {
|
||||||
|
t.Errorf("%s: unexpected error from normalization: %v", tt.test, err)
|
||||||
|
}
|
||||||
|
if err == nil && !reflect.DeepEqual(tt.config, tt.normalizedConfig) {
|
||||||
|
t.Errorf("%s: expected config to be normalized. got: %v expected: %v", tt.test, tt.config, tt.normalizedConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
plugin/pkg/admission/imagepolicy/doc.go
Normal file
18
plugin/pkg/admission/imagepolicy/doc.go
Normal 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"
|
102
plugin/pkg/admission/imagepolicy/gencerts.sh
Executable file
102
plugin/pkg/admission/imagepolicy/gencerts.sh
Executable 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
|
Loading…
Reference in New Issue
Block a user