Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
7006d224
Commit
7006d224
authored
Oct 27, 2017
by
Chao Xu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add NamespaceSelector to the api
business logic in webhook plugin and unit test add a e2e test for namespace selector
parent
7c8596a9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
510 additions
and
30 deletions
+510
-30
types.go
pkg/apis/admissionregistration/types.go
+46
-0
defaults.go
pkg/apis/admissionregistration/v1alpha1/defaults.go
+5
-0
validation.go
pkg/apis/admissionregistration/validation/validation.go
+5
-0
types.go
...ng/src/k8s.io/api/admissionregistration/v1alpha1/types.go
+46
-0
admission.go
...8s.io/apiserver/pkg/admission/plugin/webhook/admission.go
+84
-0
admission_test.go
.../apiserver/pkg/admission/plugin/webhook/admission_test.go
+180
-14
webhook.go
test/e2e/apimachinery/webhook.go
+91
-9
Makefile
test/images/webhook/Makefile
+2
-2
main.go
test/images/webhook/main.go
+51
-5
No files found.
pkg/apis/admissionregistration/types.go
View file @
7006d224
...
...
@@ -191,6 +191,52 @@ type Webhook struct {
// allowed values are Ignore or Fail. Defaults to Ignore.
// +optional
FailurePolicy
*
FailurePolicyType
// NamespaceSelector decides whether to run the webhook on an object based
// on whether the namespace for that object matches the selector. If the
// object itself is a namespace, the matching is performed on
// object.metadata.labels. If the object is other cluster scoped resource,
// it is not subjected to the webhook.
//
// For example, to run the webhook on any objects whose namespace is not
// associated with "runlevel" of "0" or "1"; you will set the selector as
// follows:
// "namespaceSelector": {
// "matchExpressions": [
// {
// "key": "runlevel",
// "operator": "NotIn",
// "values": [
// "0",
// "1"
// ]
// }
// ]
// }
//
// If instead you want to only run the webhook on any objects whose
// namespace is associated with the "environment" of "prod" or "staging";
// you will set the selector as follows:
// "namespaceSelector": {
// "matchExpressions": [
// {
// "key": "environment",
// "operator": "In",
// "values": [
// "prod",
// "staging"
// ]
// }
// ]
// }
//
// See
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
// for more examples of label selectors.
//
// Default to the empty LabelSelector, which matches everything.
// +optional
NamespaceSelector
*
metav1
.
LabelSelector
}
// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
...
...
pkg/apis/admissionregistration/v1alpha1/defaults.go
View file @
7006d224
...
...
@@ -18,6 +18,7 @@ package v1alpha1
import
(
admissionregistrationv1alpha1
"k8s.io/api/admissionregistration/v1alpha1"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
...
...
@@ -30,4 +31,8 @@ func SetDefaults_Webhook(obj *admissionregistrationv1alpha1.Webhook) {
policy
:=
admissionregistrationv1alpha1
.
Ignore
obj
.
FailurePolicy
=
&
policy
}
if
obj
.
NamespaceSelector
==
nil
{
selector
:=
metav1
.
LabelSelector
{}
obj
.
NamespaceSelector
=
&
selector
}
}
pkg/apis/admissionregistration/validation/validation.go
View file @
7006d224
...
...
@@ -21,6 +21,7 @@ import (
"strings"
genericvalidation
"k8s.io/apimachinery/pkg/api/validation"
metav1validation
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
...
...
@@ -195,6 +196,10 @@ func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) f
allErrors
=
append
(
allErrors
,
validateURLPath
(
fldPath
.
Child
(
"clientConfig"
,
"urlPath"
),
hook
.
ClientConfig
.
URLPath
)
...
)
}
if
hook
.
NamespaceSelector
!=
nil
{
allErrors
=
append
(
allErrors
,
metav1validation
.
ValidateLabelSelector
(
hook
.
NamespaceSelector
,
fldPath
.
Child
(
"namespaceSelector"
))
...
)
}
return
allErrors
}
...
...
staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go
View file @
7006d224
...
...
@@ -197,6 +197,52 @@ type Webhook struct {
// allowed values are Ignore or Fail. Defaults to Ignore.
// +optional
FailurePolicy
*
FailurePolicyType
`json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"`
// NamespaceSelector decides whether to run the webhook on an object based
// on whether the namespace for that object matches the selector. If the
// object itself is a namespace, the matching is performed on
// object.metadata.labels. If the object is other cluster scoped resource,
// it is not subjected to the webhook.
//
// For example, to run the webhook on any objects whose namespace is not
// associated with "runlevel" of "0" or "1"; you will set the selector as
// follows:
// "namespaceSelector": {
// "matchExpressions": [
// {
// "key": "runlevel",
// "operator": "NotIn",
// "values": [
// "0",
// "1"
// ]
// }
// ]
// }
//
// If instead you want to only run the webhook on any objects whose
// namespace is associated with the "environment" of "prod" or "staging";
// you will set the selector as follows:
// "namespaceSelector": {
// "matchExpressions": [
// {
// "key": "environment",
// "operator": "In",
// "values": [
// "prod",
// "staging"
// ]
// }
// ]
// }
//
// See
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
// for more examples of label selectors.
//
// Default to the empty LabelSelector, which matches everything.
// +optional
NamespaceSelector
*
metav1
.
LabelSelector
`json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"`
}
// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
...
...
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission.go
View file @
7006d224
...
...
@@ -32,7 +32,9 @@ import (
admissionv1alpha1
"k8s.io/api/admission/v1alpha1"
"k8s.io/api/admissionregistration/v1alpha1"
apierrors
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime
"k8s.io/apimachinery/pkg/util/runtime"
...
...
@@ -41,7 +43,9 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/configuration"
genericadmissioninit
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/client-go/informers"
clientset
"k8s.io/client-go/kubernetes"
corelisters
"k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/rest"
)
...
...
@@ -123,6 +127,8 @@ type GenericAdmissionWebhook struct {
hookSource
WebhookSource
serviceResolver
ServiceResolver
negotiatedSerializer
runtime
.
NegotiatedSerializer
namespaceLister
corelisters
.
NamespaceLister
client
clientset
.
Interface
authInfoResolver
AuthenticationInfoResolver
cache
*
lru
.
Cache
...
...
@@ -163,9 +169,17 @@ func (a *GenericAdmissionWebhook) SetScheme(scheme *runtime.Scheme) {
// WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it
func
(
a
*
GenericAdmissionWebhook
)
SetExternalKubeClientSet
(
client
clientset
.
Interface
)
{
a
.
client
=
client
a
.
hookSource
=
configuration
.
NewValidatingWebhookConfigurationManager
(
client
.
AdmissionregistrationV1alpha1
()
.
ValidatingWebhookConfigurations
())
}
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
func
(
a
*
GenericAdmissionWebhook
)
SetExternalKubeInformerFactory
(
f
informers
.
SharedInformerFactory
)
{
namespaceInformer
:=
f
.
Core
()
.
V1
()
.
Namespaces
()
a
.
namespaceLister
=
namespaceInformer
.
Lister
()
a
.
SetReadyFunc
(
namespaceInformer
.
Informer
()
.
HasSynced
)
}
// ValidateInitialization implements the InitializationValidator interface.
func
(
a
*
GenericAdmissionWebhook
)
ValidateInitialization
()
error
{
if
a
.
hookSource
==
nil
{
...
...
@@ -174,6 +188,9 @@ func (a *GenericAdmissionWebhook) ValidateInitialization() error {
if
a
.
negotiatedSerializer
==
nil
{
return
fmt
.
Errorf
(
"the GenericAdmissionWebhook admission plugin requires a runtime.Scheme to be provided to derive a serializer"
)
}
if
a
.
namespaceLister
==
nil
{
return
fmt
.
Errorf
(
"the GenericAdmissionWebhook admission plugin requires a namespaceLister"
)
}
go
a
.
hookSource
.
Run
(
wait
.
NeverStop
)
return
nil
}
...
...
@@ -255,7 +272,74 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
return
errs
[
0
]
}
func
(
a
*
GenericAdmissionWebhook
)
getNamespaceLabels
(
attr
admission
.
Attributes
)
(
map
[
string
]
string
,
error
)
{
// If the request itself is creating or updating a namespace, then get the
// labels from attr.Object, because namespaceLister doesn't have the latest
// namespace yet.
//
// However, if the request is deleting a namespace, then get the label from
// the namespace in the namespaceLister, because a delete request is not
// going to change the object, and attr.Object will be a DeleteOptions
// rather than a namespace object.
if
attr
.
GetResource
()
.
Resource
==
"namespaces"
&&
len
(
attr
.
GetSubresource
())
==
0
&&
(
attr
.
GetOperation
()
==
admission
.
Create
||
attr
.
GetOperation
()
==
admission
.
Update
)
{
accessor
,
err
:=
meta
.
Accessor
(
attr
.
GetObject
())
if
err
!=
nil
{
return
nil
,
err
}
return
accessor
.
GetLabels
(),
nil
}
namespaceName
:=
attr
.
GetNamespace
()
namespace
,
err
:=
a
.
namespaceLister
.
Get
(
namespaceName
)
if
err
!=
nil
&&
!
apierrors
.
IsNotFound
(
err
)
{
return
nil
,
err
}
if
apierrors
.
IsNotFound
(
err
)
{
// in case of latency in our caches, make a call direct to storage to verify that it truly exists or not
namespace
,
err
=
a
.
client
.
Core
()
.
Namespaces
()
.
Get
(
namespaceName
,
metav1
.
GetOptions
{})
if
err
!=
nil
{
return
nil
,
err
}
}
return
namespace
.
Labels
,
nil
}
// whether the request is exempted by the webhook because of the
// namespaceSelector of the webhook.
func
(
a
*
GenericAdmissionWebhook
)
exemptedByNamespaceSelector
(
h
*
v1alpha1
.
Webhook
,
attr
admission
.
Attributes
)
(
bool
,
error
)
{
namespaceName
:=
attr
.
GetNamespace
()
if
len
(
namespaceName
)
==
0
&&
attr
.
GetResource
()
.
Resource
!=
"namespaces"
{
// If the request is about a cluster scoped resource, and it is not a
// namespace, it is exempted from all webhooks for now.
// TODO: figure out a way selective exempt cluster scoped resources.
// Also update the comment in types.go
return
true
,
nil
}
namespaceLabels
,
err
:=
a
.
getNamespaceLabels
(
attr
)
if
apierrors
.
IsNotFound
(
err
)
{
return
false
,
err
}
if
err
!=
nil
{
return
false
,
apierrors
.
NewInternalError
(
err
)
}
// TODO: adding an LRU cache to cache the translation
selector
,
err
:=
metav1
.
LabelSelectorAsSelector
(
h
.
NamespaceSelector
)
if
err
!=
nil
{
return
false
,
apierrors
.
NewInternalError
(
err
)
}
return
!
selector
.
Matches
(
labels
.
Set
(
namespaceLabels
)),
nil
}
func
(
a
*
GenericAdmissionWebhook
)
callHook
(
ctx
context
.
Context
,
h
*
v1alpha1
.
Webhook
,
attr
admission
.
Attributes
)
error
{
excluded
,
err
:=
a
.
exemptedByNamespaceSelector
(
h
,
attr
)
if
err
!=
nil
{
return
err
}
if
excluded
{
return
nil
}
matches
:=
false
for
_
,
r
:=
range
h
.
Rules
{
m
:=
RuleMatcher
{
Rule
:
r
,
Attr
:
attr
}
...
...
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/admission_test.go
View file @
7006d224
...
...
@@ -24,16 +24,20 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"sync/atomic"
"testing"
"k8s.io/api/admission/v1alpha1"
registrationv1alpha1
"k8s.io/api/admissionregistration/v1alpha1"
api
"k8s.io/api/core/v1"
corev1
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors
"k8s.io/apimachinery/pkg/api/errors"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest"
...
...
@@ -48,6 +52,11 @@ func (f *fakeHookSource) Webhooks() (*registrationv1alpha1.ValidatingWebhookConf
if
f
.
err
!=
nil
{
return
nil
,
f
.
err
}
for
i
,
h
:=
range
f
.
hooks
{
if
h
.
NamespaceSelector
==
nil
{
f
.
hooks
[
i
]
.
NamespaceSelector
=
&
metav1
.
LabelSelector
{}
}
}
return
&
registrationv1alpha1
.
ValidatingWebhookConfiguration
{
Webhooks
:
f
.
hooks
},
nil
}
...
...
@@ -65,11 +74,26 @@ func (f fakeServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL,
return
&
u
,
nil
}
type
fakeNamespaceLister
struct
{
namespaces
map
[
string
]
*
corev1
.
Namespace
}
func
(
f
fakeNamespaceLister
)
List
(
selector
labels
.
Selector
)
(
ret
[]
*
corev1
.
Namespace
,
err
error
)
{
return
nil
,
nil
}
func
(
f
fakeNamespaceLister
)
Get
(
name
string
)
(
*
corev1
.
Namespace
,
error
)
{
ns
,
ok
:=
f
.
namespaces
[
name
]
if
ok
{
return
ns
,
nil
}
return
nil
,
errors
.
NewNotFound
(
corev1
.
Resource
(
"namespaces"
),
name
)
}
// TestAdmit tests that GenericAdmissionWebhook#Admit works as expected
func
TestAdmit
(
t
*
testing
.
T
)
{
scheme
:=
runtime
.
NewScheme
()
v1alpha1
.
AddToScheme
(
scheme
)
api
.
AddToScheme
(
scheme
)
corev1
.
AddToScheme
(
scheme
)
testServer
:=
newTestServer
(
t
)
testServer
.
StartTLS
()
...
...
@@ -85,12 +109,22 @@ func TestAdmit(t *testing.T) {
wh
.
authInfoResolver
=
newFakeAuthenticationInfoResolver
()
wh
.
serviceResolver
=
fakeServiceResolver
{
base
:
*
serverURL
}
wh
.
SetScheme
(
scheme
)
namespace
:=
"webhook-test"
wh
.
namespaceLister
=
fakeNamespaceLister
{
map
[
string
]
*
corev1
.
Namespace
{
namespace
:
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Labels
:
map
[
string
]
string
{
"runlevel"
:
"0"
,
},
},
},
},
}
// Set up a test object for the call
kind
:=
api
.
SchemeGroupVersion
.
WithKind
(
"Pod"
)
kind
:=
corev1
.
SchemeGroupVersion
.
WithKind
(
"Pod"
)
name
:=
"my-pod"
namespace
:=
"webhook-test"
object
:=
api
.
Pod
{
object
:=
corev1
.
Pod
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Labels
:
map
[
string
]
string
{
"pod.name"
:
name
,
...
...
@@ -103,11 +137,11 @@ func TestAdmit(t *testing.T) {
Kind
:
"Pod"
,
},
}
oldObject
:=
api
.
Pod
{
oldObject
:=
corev1
.
Pod
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
name
,
Namespace
:
namespace
},
}
operation
:=
admission
.
Update
resource
:=
api
.
Resource
(
"pods"
)
.
WithVersion
(
"v1"
)
resource
:=
corev1
.
Resource
(
"pods"
)
.
WithVersion
(
"v1"
)
subResource
:=
""
userInfo
:=
user
.
DefaultInfo
{
Name
:
"webhook-test"
,
...
...
@@ -167,6 +201,40 @@ func TestAdmit(t *testing.T) {
},
errorContains
:
"you shall not pass"
,
},
"match & disallow & but allowed because namespaceSelector exempt the namespace"
:
{
hookSource
:
fakeHookSource
{
hooks
:
[]
registrationv1alpha1
.
Webhook
{{
Name
:
"disallow"
,
ClientConfig
:
newFakeHookClientConfig
(
"disallow"
),
Rules
:
newMatchEverythingRules
(),
NamespaceSelector
:
&
metav1
.
LabelSelector
{
MatchExpressions
:
[]
metav1
.
LabelSelectorRequirement
{{
Key
:
"runlevel"
,
Values
:
[]
string
{
"1"
},
Operator
:
metav1
.
LabelSelectorOpIn
,
}},
},
}},
},
expectAllow
:
true
,
},
"match & disallow & but allowed because namespaceSelector exempt the namespace ii"
:
{
hookSource
:
fakeHookSource
{
hooks
:
[]
registrationv1alpha1
.
Webhook
{{
Name
:
"disallow"
,
ClientConfig
:
newFakeHookClientConfig
(
"disallow"
),
Rules
:
newMatchEverythingRules
(),
NamespaceSelector
:
&
metav1
.
LabelSelector
{
MatchExpressions
:
[]
metav1
.
LabelSelectorRequirement
{{
Key
:
"runlevel"
,
Values
:
[]
string
{
"0"
},
Operator
:
metav1
.
LabelSelectorOpNotIn
,
}},
},
}},
},
expectAllow
:
true
,
},
"match & fail (but allow because fail open)"
:
{
hookSource
:
fakeHookSource
{
hooks
:
[]
registrationv1alpha1
.
Webhook
{{
...
...
@@ -230,9 +298,11 @@ func TestAdmit(t *testing.T) {
}
for
name
,
tt
:=
range
table
{
if
!
strings
.
Contains
(
name
,
"no match"
)
{
continue
}
t
.
Run
(
name
,
func
(
t
*
testing
.
T
)
{
wh
.
hookSource
=
&
tt
.
hookSource
err
=
wh
.
Admit
(
admission
.
NewAttributesRecord
(
&
object
,
&
oldObject
,
kind
,
namespace
,
name
,
resource
,
subResource
,
operation
,
&
userInfo
))
if
tt
.
expectAllow
!=
(
err
==
nil
)
{
t
.
Errorf
(
"expected allowed=%v, but got err=%v"
,
tt
.
expectAllow
,
err
)
...
...
@@ -254,7 +324,7 @@ func TestAdmit(t *testing.T) {
func
TestAdmitCachedClient
(
t
*
testing
.
T
)
{
scheme
:=
runtime
.
NewScheme
()
v1alpha1
.
AddToScheme
(
scheme
)
api
.
AddToScheme
(
scheme
)
corev1
.
AddToScheme
(
scheme
)
testServer
:=
newTestServer
(
t
)
testServer
.
StartTLS
()
...
...
@@ -270,12 +340,22 @@ func TestAdmitCachedClient(t *testing.T) {
wh
.
authInfoResolver
=
newFakeAuthenticationInfoResolver
()
wh
.
serviceResolver
=
fakeServiceResolver
{
base
:
*
serverURL
}
wh
.
SetScheme
(
scheme
)
namespace
:=
"webhook-test"
wh
.
namespaceLister
=
fakeNamespaceLister
{
map
[
string
]
*
corev1
.
Namespace
{
namespace
:
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Labels
:
map
[
string
]
string
{
"runlevel"
:
"0"
,
},
},
},
},
}
// Set up a test object for the call
kind
:=
api
.
SchemeGroupVersion
.
WithKind
(
"Pod"
)
kind
:=
corev1
.
SchemeGroupVersion
.
WithKind
(
"Pod"
)
name
:=
"my-pod"
namespace
:=
"webhook-test"
object
:=
api
.
Pod
{
object
:=
corev1
.
Pod
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Labels
:
map
[
string
]
string
{
"pod.name"
:
name
,
...
...
@@ -288,11 +368,11 @@ func TestAdmitCachedClient(t *testing.T) {
Kind
:
"Pod"
,
},
}
oldObject
:=
api
.
Pod
{
oldObject
:=
corev1
.
Pod
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
name
,
Namespace
:
namespace
},
}
operation
:=
admission
.
Update
resource
:=
api
.
Resource
(
"pods"
)
.
WithVersion
(
"v1"
)
resource
:=
corev1
.
Resource
(
"pods"
)
.
WithVersion
(
"v1"
)
subResource
:=
""
userInfo
:=
user
.
DefaultInfo
{
Name
:
"webhook-test"
,
...
...
@@ -522,3 +602,89 @@ func newMatchEverythingRules() []registrationv1alpha1.RuleWithOperations {
},
}}
}
func
TestGetNamespaceLabels
(
t
*
testing
.
T
)
{
namespace1Labels
:=
map
[
string
]
string
{
"runlevel"
:
"1"
,
}
namespace1
:=
corev1
.
Namespace
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
"1"
,
Labels
:
namespace1Labels
,
},
}
namespace2Labels
:=
map
[
string
]
string
{
"runlevel"
:
"2"
,
}
namespace2
:=
corev1
.
Namespace
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
"2"
,
Labels
:
namespace2Labels
,
},
}
namespaceLister
:=
fakeNamespaceLister
{
map
[
string
]
*
corev1
.
Namespace
{
"1"
:
&
namespace1
,
},
}
tests
:=
[]
struct
{
name
string
attr
admission
.
Attributes
expectedLabels
map
[
string
]
string
}{
{
name
:
"request is for creating namespace, the labels should be from the object itself"
,
attr
:
admission
.
NewAttributesRecord
(
&
namespace2
,
nil
,
schema
.
GroupVersionKind
{},
""
,
namespace2
.
Name
,
schema
.
GroupVersionResource
{
Resource
:
"namespaces"
},
""
,
admission
.
Create
,
nil
),
expectedLabels
:
namespace2Labels
,
},
{
name
:
"request is for updating namespace, the labels should be from the new object"
,
attr
:
admission
.
NewAttributesRecord
(
&
namespace2
,
nil
,
schema
.
GroupVersionKind
{},
namespace2
.
Name
,
namespace2
.
Name
,
schema
.
GroupVersionResource
{
Resource
:
"namespaces"
},
""
,
admission
.
Update
,
nil
),
expectedLabels
:
namespace2Labels
,
},
{
name
:
"request is for deleting namespace, the labels should be from the cache"
,
attr
:
admission
.
NewAttributesRecord
(
&
namespace2
,
nil
,
schema
.
GroupVersionKind
{},
namespace1
.
Name
,
namespace1
.
Name
,
schema
.
GroupVersionResource
{
Resource
:
"namespaces"
},
""
,
admission
.
Delete
,
nil
),
expectedLabels
:
namespace1Labels
,
},
{
name
:
"request is for namespace/finalizer"
,
attr
:
admission
.
NewAttributesRecord
(
nil
,
nil
,
schema
.
GroupVersionKind
{},
namespace1
.
Name
,
"mock-name"
,
schema
.
GroupVersionResource
{
Resource
:
"namespaces"
},
"finalizers"
,
admission
.
Create
,
nil
),
expectedLabels
:
namespace1Labels
,
},
{
name
:
"request is for pod"
,
attr
:
admission
.
NewAttributesRecord
(
nil
,
nil
,
schema
.
GroupVersionKind
{},
namespace1
.
Name
,
"mock-name"
,
schema
.
GroupVersionResource
{
Resource
:
"pods"
},
""
,
admission
.
Create
,
nil
),
expectedLabels
:
namespace1Labels
,
},
}
wh
,
err
:=
NewGenericAdmissionWebhook
(
nil
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
wh
.
namespaceLister
=
namespaceLister
for
_
,
tt
:=
range
tests
{
actualLabels
,
err
:=
wh
.
getNamespaceLabels
(
tt
.
attr
)
if
err
!=
nil
{
t
.
Error
(
err
)
}
if
!
reflect
.
DeepEqual
(
actualLabels
,
tt
.
expectedLabels
)
{
t
.
Errorf
(
"expected labels to be %#v, got %#v"
,
tt
.
expectedLabels
,
actualLabels
)
}
}
}
func
TestExemptClusterScopedResource
(
t
*
testing
.
T
)
{
hook
:=
&
registrationv1alpha1
.
Webhook
{
NamespaceSelector
:
&
metav1
.
LabelSelector
{},
}
attr
:=
admission
.
NewAttributesRecord
(
nil
,
nil
,
schema
.
GroupVersionKind
{},
""
,
"mock-name"
,
schema
.
GroupVersionResource
{
Version
:
"v1"
,
Resource
:
"nodes"
},
""
,
admission
.
Create
,
nil
)
g
:=
GenericAdmissionWebhook
{}
exempted
,
err
:=
g
.
exemptedByNamespaceSelector
(
hook
,
attr
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
!
exempted
{
t
.
Errorf
(
"cluster scoped resources (but not a namespace) should be exempted from all webhooks"
)
}
}
test/e2e/apimachinery/webhook.go
View file @
7006d224
...
...
@@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/wait"
utilversion
"k8s.io/kubernetes/pkg/util/version"
"k8s.io/kubernetes/test/e2e/framework"
...
...
@@ -36,11 +37,16 @@ import (
)
const
(
secretName
=
"sample-webhook-secret"
deploymentName
=
"sample-webhook-deployment"
serviceName
=
"e2e-test-webhook"
roleBindingName
=
"webhook-auth-reader"
webhookConfigName
=
"e2e-test-webhook-config"
secretName
=
"sample-webhook-secret"
deploymentName
=
"sample-webhook-deployment"
serviceName
=
"e2e-test-webhook"
roleBindingName
=
"webhook-auth-reader"
webhookConfigName
=
"e2e-test-webhook-config"
skipNamespaceLabelKey
=
"skip-webhook-admission"
skipNamespaceLabelValue
=
"yes"
skippedNamespaceName
=
"exempted-namesapce"
disallowedPodName
=
"disallowed-pod"
disallowedConfigMapName
=
"disallowed-configmap"
)
var
serverWebhookVersion
=
utilversion
.
MustParseSemantic
(
"v1.8.0"
)
...
...
@@ -51,7 +57,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
cleanWebhookTest
(
f
)
})
It
(
"Should be able to deny pod creation"
,
func
()
{
It
(
"Should be able to deny pod
and configmap
creation"
,
func
()
{
// Make sure the relevant provider supports admission webhook
framework
.
SkipUnlessServerVersionGTE
(
serverWebhookVersion
,
f
.
ClientSet
.
Discovery
())
framework
.
SkipUnlessProviderIs
(
"gce"
,
"gke"
)
...
...
@@ -68,7 +74,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
// Note that in 1.9 we will have backwards incompatible change to
// admission webhooks, so the image will be updated to 1.9 sometime in
// the development 1.9 cycle.
deployWebhookAndService
(
f
,
"gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v
1
"
,
context
)
deployWebhookAndService
(
f
,
"gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v
2
"
,
context
)
registerWebhook
(
f
,
context
)
testWebhook
(
f
)
})
...
...
@@ -223,7 +229,7 @@ func registerWebhook(f *framework.Framework, context *certContext) {
},
Webhooks
:
[]
v1alpha1
.
Webhook
{
{
Name
:
"
e2e-test-webhook
.k8s.io"
,
Name
:
"
deny-unwanted-pod-container-name-and-label
.k8s.io"
,
Rules
:
[]
v1alpha1
.
RuleWithOperations
{{
Operations
:
[]
v1alpha1
.
OperationType
{
v1alpha1
.
Create
},
Rule
:
v1alpha1
.
Rule
{
...
...
@@ -237,6 +243,36 @@ func registerWebhook(f *framework.Framework, context *certContext) {
Namespace
:
namespace
,
Name
:
serviceName
,
},
URLPath
:
"/pods"
,
CABundle
:
context
.
signingCert
,
},
},
{
Name
:
"deny-unwanted-configmap-data.k8s.io"
,
Rules
:
[]
v1alpha1
.
RuleWithOperations
{{
Operations
:
[]
v1alpha1
.
OperationType
{
v1alpha1
.
Create
},
Rule
:
v1alpha1
.
Rule
{
APIGroups
:
[]
string
{
""
},
APIVersions
:
[]
string
{
"v1"
},
Resources
:
[]
string
{
"configmaps"
},
},
}},
// The webhook skips the namespace that has label "skip-webhook-admission":"yes"
NamespaceSelector
:
&
metav1
.
LabelSelector
{
MatchExpressions
:
[]
metav1
.
LabelSelectorRequirement
{
{
Key
:
skipNamespaceLabelKey
,
Operator
:
metav1
.
LabelSelectorOpNotIn
,
Values
:
[]
string
{
skipNamespaceLabelValue
},
},
},
},
ClientConfig
:
v1alpha1
.
WebhookClientConfig
{
Service
:
v1alpha1
.
ServiceReference
{
Namespace
:
namespace
,
Name
:
serviceName
,
},
URLPath
:
"/configmaps"
,
CABundle
:
context
.
signingCert
,
},
},
...
...
@@ -262,12 +298,45 @@ func testWebhook(f *framework.Framework) {
// TODO: Test if webhook can detect pod with non-compliant metadata.
// Currently metadata is lost because webhook uses the external version of
// the objects, and the apiserver sends the internal objects.
By
(
"create a configmap that should be denied by the webhook"
)
// Creating the configmap, the request should be rejected
configmap
:=
nonCompliantConfigMap
(
f
)
_
,
err
=
client
.
CoreV1
()
.
ConfigMaps
(
f
.
Namespace
.
Name
)
.
Create
(
configmap
)
Expect
(
err
)
.
NotTo
(
BeNil
())
expectedErrMsg
=
"the configmap contains unwanted key and value"
if
!
strings
.
Contains
(
err
.
Error
(),
expectedErrMsg
)
{
framework
.
Failf
(
"expect error contains %q, got %q"
,
expectedErrMsg
,
err
.
Error
())
}
By
(
"create a namespace that bypass the webhook"
)
err
=
wait
.
Poll
(
100
*
time
.
Millisecond
,
30
*
time
.
Second
,
func
()
(
bool
,
error
)
{
_
,
err2
:=
client
.
CoreV1
()
.
Namespaces
()
.
Create
(
&
v1
.
Namespace
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
skippedNamespaceName
,
Labels
:
map
[
string
]
string
{
skipNamespaceLabelKey
:
skipNamespaceLabelValue
,
},
}})
if
err2
!=
nil
{
if
strings
.
HasPrefix
(
err2
.
Error
(),
"object is being deleted:"
)
{
return
false
,
nil
}
return
false
,
err2
}
return
true
,
nil
})
framework
.
ExpectNoError
(
err
,
"creating namespace %q"
,
skippedNamespaceName
)
By
(
"create a configmap that violates the webhook policy but is in a whitelisted namespace"
)
configmap
=
nonCompliantConfigMap
(
f
)
_
,
err
=
client
.
CoreV1
()
.
ConfigMaps
(
skippedNamespaceName
)
.
Create
(
configmap
)
Expect
(
err
)
.
To
(
BeNil
())
}
func
nonCompliantPod
(
f
*
framework
.
Framework
)
*
v1
.
Pod
{
return
&
v1
.
Pod
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
"disallowed-pod"
,
Name
:
disallowedPodName
,
Labels
:
map
[
string
]
string
{
"webhook-e2e-test"
:
"disallow"
,
},
...
...
@@ -283,6 +352,17 @@ func nonCompliantPod(f *framework.Framework) *v1.Pod {
}
}
func
nonCompliantConfigMap
(
f
*
framework
.
Framework
)
*
v1
.
ConfigMap
{
return
&
v1
.
ConfigMap
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
disallowedConfigMapName
,
},
Data
:
map
[
string
]
string
{
"webhook-e2e-test"
:
"webhook-disallow"
,
},
}
}
func
cleanWebhookTest
(
f
*
framework
.
Framework
)
{
client
:=
f
.
ClientSet
_
=
client
.
AdmissionregistrationV1alpha1
()
.
ValidatingWebhookConfigurations
()
.
Delete
(
webhookConfigName
,
nil
)
...
...
@@ -291,4 +371,6 @@ func cleanWebhookTest(f *framework.Framework) {
_
=
client
.
ExtensionsV1beta1
()
.
Deployments
(
namespaceName
)
.
Delete
(
deploymentName
,
nil
)
_
=
client
.
CoreV1
()
.
Secrets
(
namespaceName
)
.
Delete
(
secretName
,
nil
)
_
=
client
.
RbacV1beta1
()
.
RoleBindings
(
"kube-system"
)
.
Delete
(
roleBindingName
,
nil
)
_
=
client
.
CoreV1
()
.
ConfigMaps
(
skippedNamespaceName
)
.
Delete
(
disallowedConfigMapName
,
nil
)
_
=
client
.
CoreV1
()
.
Namespaces
()
.
Delete
(
skippedNamespaceName
,
nil
)
}
test/images/webhook/Makefile
View file @
7006d224
...
...
@@ -14,6 +14,6 @@
build
:
CGO_ENABLED
=
0
GOOS
=
linux go build
-a
-installsuffix
cgo
-o
webhook .
docker build
--no-cache
-t
gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v
1
.
docker build
--no-cache
-t
gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v
2
.
push
:
gcloud docker
--
push
gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v1 .
gcloud docker
--
push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v2
test/images/webhook/main.go
View file @
7006d224
...
...
@@ -44,7 +44,8 @@ func (c *Config) addFlags() {
}
// only allow pods to pull images from specific registry.
func
admit
(
data
[]
byte
)
*
v1alpha1
.
AdmissionReviewStatus
{
func
admitPods
(
data
[]
byte
)
*
v1alpha1
.
AdmissionReviewStatus
{
glog
.
V
(
2
)
.
Info
(
"admitting pods"
)
ar
:=
v1alpha1
.
AdmissionReview
{}
if
err
:=
json
.
Unmarshal
(
data
,
&
ar
);
err
!=
nil
{
glog
.
Error
(
err
)
...
...
@@ -86,7 +87,42 @@ func admit(data []byte) *v1alpha1.AdmissionReviewStatus {
return
&
reviewStatus
}
func
serve
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
// deny configmaps with specific key-value pair.
func
admitConfigMaps
(
data
[]
byte
)
*
v1alpha1
.
AdmissionReviewStatus
{
glog
.
V
(
2
)
.
Info
(
"admitting configmaps"
)
ar
:=
v1alpha1
.
AdmissionReview
{}
if
err
:=
json
.
Unmarshal
(
data
,
&
ar
);
err
!=
nil
{
glog
.
Error
(
err
)
return
nil
}
configMapResource
:=
metav1
.
GroupVersionResource
{
Group
:
""
,
Version
:
"v1"
,
Resource
:
"configmaps"
}
if
ar
.
Spec
.
Resource
!=
configMapResource
{
glog
.
Errorf
(
"expect resource to be %s"
,
configMapResource
)
return
nil
}
raw
:=
ar
.
Spec
.
Object
.
Raw
configmap
:=
v1
.
ConfigMap
{}
if
err
:=
json
.
Unmarshal
(
raw
,
&
configmap
);
err
!=
nil
{
glog
.
Error
(
err
)
return
nil
}
reviewStatus
:=
v1alpha1
.
AdmissionReviewStatus
{}
reviewStatus
.
Allowed
=
true
for
k
,
v
:=
range
configmap
.
Data
{
if
k
==
"webhook-e2e-test"
&&
v
==
"webhook-disallow"
{
reviewStatus
.
Allowed
=
false
reviewStatus
.
Result
=
&
metav1
.
Status
{
Reason
:
"the configmap contains unwanted key and value"
,
}
}
}
return
&
reviewStatus
}
type
admitFunc
func
(
data
[]
byte
)
*
v1alpha1
.
AdmissionReviewStatus
func
serve
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
,
admit
admitFunc
)
{
var
body
[]
byte
if
r
.
Body
!=
nil
{
if
data
,
err
:=
ioutil
.
ReadAll
(
r
.
Body
);
err
==
nil
{
...
...
@@ -102,8 +138,10 @@ func serve(w http.ResponseWriter, r *http.Request) {
}
reviewStatus
:=
admit
(
body
)
ar
:=
v1alpha1
.
AdmissionReview
{
Status
:
*
reviewStatus
,
ar
:=
v1alpha1
.
AdmissionReview
{}
if
reviewStatus
!=
nil
{
ar
.
Status
=
*
reviewStatus
}
resp
,
err
:=
json
.
Marshal
(
ar
)
...
...
@@ -115,12 +153,20 @@ func serve(w http.ResponseWriter, r *http.Request) {
}
}
func
servePods
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
serve
(
w
,
r
,
admitPods
)
}
func
serveConfigmaps
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
serve
(
w
,
r
,
admitConfigMaps
)
}
func
main
()
{
var
config
Config
config
.
addFlags
()
flag
.
Parse
()
http
.
HandleFunc
(
"/"
,
serve
)
http
.
HandleFunc
(
"/pods"
,
servePods
)
http
.
HandleFunc
(
"/configmaps"
,
serveConfigmaps
)
clientset
:=
getClient
()
server
:=
&
http
.
Server
{
Addr
:
":443"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment