Unverified Commit 8b052158 authored by Kubernetes Prow Robot's avatar Kubernetes Prow Robot Committed by GitHub

Merge pull request #74477 from liggitt/webhook-admission-scope

Webhook admission scope
parents 9f7560de 1d4655c8
......@@ -99,6 +99,10 @@
"type": "string"
},
"type": "array"
},
"scope": {
"description": "scope specifies the scope of this rule. Valid values are \"Cluster\", \"Namespaced\", and \"*\" \"Cluster\" means that only cluster-scoped resources will match this rule. Namespace API objects are cluster-scoped. \"Namespaced\" means that only namespaced resources will match this rule. \"*\" means that there are no scope restrictions. Subresources match the scope of their parent resource. Default is \"*\".",
"type": "string"
}
},
"type": "object"
......@@ -26,6 +26,13 @@ import (
// Funcs returns the fuzzer functions for the admissionregistration api group.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(obj *admissionregistration.Rule, c fuzz.Continue) {
c.FuzzNoCustom(obj) // fuzz self without calling this function again
if obj.Scope == nil {
s := admissionregistration.AllScopes
obj.Scope = &s
}
},
func(obj *admissionregistration.Webhook, c fuzz.Continue) {
c.FuzzNoCustom(obj) // fuzz self without calling this function again
p := admissionregistration.FailurePolicyType("Fail")
......
......@@ -49,8 +49,32 @@ type Rule struct {
// Depending on the enclosing object, subresources might not be allowed.
// Required.
Resources []string
// scope specifies the scope of this rule.
// Valid values are "Cluster", "Namespaced", and "*"
// "Cluster" means that only cluster-scoped resources will match this rule.
// Namespace API objects are cluster-scoped.
// "Namespaced" means that only namespaced resources will match this rule.
// "*" means that there are no scope restrictions.
// Subresources match the scope of their parent resource.
// Default is "*".
//
// +optional
Scope *ScopeType
}
type ScopeType string
const (
// ClusterScope means that scope is limited to cluster-scoped objects.
// Namespace objects are cluster-scoped.
ClusterScope ScopeType = "Cluster"
// NamespacedScope means that scope is limited to namespaced objects.
NamespacedScope ScopeType = "Namespaced"
// AllScopes means that all scopes are included.
AllScopes ScopeType = "*"
)
type FailurePolicyType string
const (
......
......@@ -45,3 +45,10 @@ func SetDefaults_Webhook(obj *admissionregistrationv1beta1.Webhook) {
*obj.TimeoutSeconds = 30
}
}
func SetDefaults_Rule(obj *admissionregistrationv1beta1.Rule) {
if obj.Scope == nil {
s := admissionregistrationv1beta1.AllScopes
obj.Scope = &s
}
}
......@@ -178,6 +178,7 @@ func autoConvert_v1beta1_Rule_To_admissionregistration_Rule(in *v1beta1.Rule, ou
out.APIGroups = *(*[]string)(unsafe.Pointer(&in.APIGroups))
out.APIVersions = *(*[]string)(unsafe.Pointer(&in.APIVersions))
out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources))
out.Scope = (*admissionregistration.ScopeType)(unsafe.Pointer(in.Scope))
return nil
}
......@@ -190,6 +191,7 @@ func autoConvert_admissionregistration_Rule_To_v1beta1_Rule(in *admissionregistr
out.APIGroups = *(*[]string)(unsafe.Pointer(&in.APIGroups))
out.APIVersions = *(*[]string)(unsafe.Pointer(&in.APIVersions))
out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources))
out.Scope = (*v1beta1.ScopeType)(unsafe.Pointer(in.Scope))
return nil
}
......
......@@ -48,6 +48,10 @@ func SetObjectDefaults_MutatingWebhookConfiguration(in *v1beta1.MutatingWebhookC
for i := range in.Webhooks {
a := &in.Webhooks[i]
SetDefaults_Webhook(a)
for j := range a.Rules {
b := &a.Rules[j]
SetDefaults_Rule(&b.Rule)
}
}
}
......@@ -62,6 +66,10 @@ func SetObjectDefaults_ValidatingWebhookConfiguration(in *v1beta1.ValidatingWebh
for i := range in.Webhooks {
a := &in.Webhooks[i]
SetDefaults_Webhook(a)
for j := range a.Rules {
b := &a.Rules[j]
SetDefaults_Rule(&b.Rule)
}
}
}
......
......@@ -113,6 +113,12 @@ func validateResourcesNoSubResources(resources []string, fldPath *field.Path) fi
return allErrors
}
var validScopes = sets.NewString(
string(admissionregistration.ClusterScope),
string(admissionregistration.NamespacedScope),
string(admissionregistration.AllScopes),
)
func validateRule(rule *admissionregistration.Rule, fldPath *field.Path, allowSubResource bool) field.ErrorList {
var allErrors field.ErrorList
if len(rule.APIGroups) == 0 {
......@@ -138,6 +144,9 @@ func validateRule(rule *admissionregistration.Rule, fldPath *field.Path, allowSu
} else {
allErrors = append(allErrors, validateResourcesNoSubResources(rule.Resources, fldPath.Child("resources"))...)
}
if rule.Scope != nil && !validScopes.Has(string(*rule.Scope)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("scope"), *rule.Scope, validScopes.List()))
}
return allErrors
}
......
......@@ -109,6 +109,11 @@ func (in *Rule) DeepCopyInto(out *Rule) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Scope != nil {
in, out := &in.Scope, &out.Scope
*out = new(ScopeType)
**out = **in
}
return
}
......
......@@ -81,6 +81,18 @@ message Rule {
// Depending on the enclosing object, subresources might not be allowed.
// Required.
repeated string resources = 3;
// scope specifies the scope of this rule.
// Valid values are "Cluster", "Namespaced", and "*"
// "Cluster" means that only cluster-scoped resources will match this rule.
// Namespace API objects are cluster-scoped.
// "Namespaced" means that only namespaced resources will match this rule.
// "*" means that there are no scope restrictions.
// Subresources match the scope of their parent resource.
// Default is "*".
//
// +optional
optional string scope = 4;
}
// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
......
......@@ -49,8 +49,32 @@ type Rule struct {
// Depending on the enclosing object, subresources might not be allowed.
// Required.
Resources []string `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"`
// scope specifies the scope of this rule.
// Valid values are "Cluster", "Namespaced", and "*"
// "Cluster" means that only cluster-scoped resources will match this rule.
// Namespace API objects are cluster-scoped.
// "Namespaced" means that only namespaced resources will match this rule.
// "*" means that there are no scope restrictions.
// Subresources match the scope of their parent resource.
// Default is "*".
//
// +optional
Scope *ScopeType `json:"scope,omitempty" protobuf:"bytes,4,rep,name=scope"`
}
type ScopeType string
const (
// ClusterScope means that scope is limited to cluster-scoped objects.
// Namespace objects are cluster-scoped.
ClusterScope ScopeType = "Cluster"
// NamespacedScope means that scope is limited to namespaced objects.
NamespacedScope ScopeType = "Namespaced"
// AllScopes means that all scopes are included.
AllScopes ScopeType = "*"
)
type FailurePolicyType string
const (
......
......@@ -52,6 +52,7 @@ var map_Rule = map[string]string{
"apiGroups": "APIGroups is the API groups the resources belong to. '*' is all groups. If '*' is present, the length of the slice must be one. Required.",
"apiVersions": "APIVersions is the API versions the resources belong to. '*' is all versions. If '*' is present, the length of the slice must be one. Required.",
"resources": "Resources is a list of resources this rule applies to.\n\nFor example: 'pods' means pods. 'pods/log' means the log subresource of pods. '*' means all resources, but not subresources. 'pods/*' means all subresources of pods. '*/scale' means all scale subresources. '*/*' means all resources and their subresources.\n\nIf wildcard is present, the validation rule will ensure resources do not overlap with each other.\n\nDepending on the enclosing object, subresources might not be allowed. Required.",
"scope": "scope specifies the scope of this rule. Valid values are \"Cluster\", \"Namespaced\", and \"*\" \"Cluster\" means that only cluster-scoped resources will match this rule. Namespace API objects are cluster-scoped. \"Namespaced\" means that only namespaced resources will match this rule. \"*\" means that there are no scope restrictions. Subresources match the scope of their parent resource. Default is \"*\".",
}
func (Rule) SwaggerDoc() map[string]string {
......
......@@ -109,6 +109,11 @@ func (in *Rule) DeepCopyInto(out *Rule) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Scope != nil {
in, out := &in.Scope, &out.Scope
*out = new(ScopeType)
**out = **in
}
return
}
......
......@@ -8,6 +8,8 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
......@@ -19,6 +21,7 @@ go_test(
deps = [
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
......
......@@ -20,6 +20,8 @@ import (
"strings"
"k8s.io/api/admissionregistration/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
)
......@@ -31,7 +33,8 @@ type Matcher struct {
// Matches returns if the Attr matches the Rule.
func (r *Matcher) Matches() bool {
return r.operation() &&
return r.scope() &&
r.operation() &&
r.group() &&
r.version() &&
r.resource()
......@@ -50,6 +53,25 @@ func exactOrWildcard(items []string, requested string) bool {
return false
}
var namespaceResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"}
func (r *Matcher) scope() bool {
if r.Rule.Scope == nil || *r.Rule.Scope == v1beta1.AllScopes {
return true
}
// attr.GetNamespace() is set to the name of the namespace for requests of the namespace object itself.
switch *r.Rule.Scope {
case v1beta1.NamespacedScope:
// first make sure that we are not requesting a namespace object (namespace objects are cluster-scoped)
return r.Attr.GetResource() != namespaceResource && r.Attr.GetNamespace() != metav1.NamespaceNone
case v1beta1.ClusterScope:
// also return true if the request is for a namespace object (namespace objects are cluster-scoped)
return r.Attr.GetResource() == namespaceResource || r.Attr.GetNamespace() == metav1.NamespaceNone
default:
return false
}
}
func (r *Matcher) group() bool {
return exactOrWildcard(r.Rule.APIGroups, r.Attr.GetResource().Group)
}
......
......@@ -17,10 +17,12 @@ limitations under the License.
package rules
import (
"fmt"
"testing"
adreg "k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
)
......@@ -43,6 +45,30 @@ func a(group, version, resource, subresource, name string, operation admission.O
)
}
func namespacedAttributes(group, version, resource, subresource, name string, operation admission.Operation) admission.Attributes {
return admission.NewAttributesRecord(
nil, nil,
schema.GroupVersionKind{Group: group, Version: version, Kind: "k" + resource},
"ns", name,
schema.GroupVersionResource{Group: group, Version: version, Resource: resource}, subresource,
operation,
false,
nil,
)
}
func clusterScopedAttributes(group, version, resource, subresource, name string, operation admission.Operation) admission.Attributes {
return admission.NewAttributesRecord(
nil, nil,
schema.GroupVersionKind{Group: group, Version: version, Kind: "k" + resource},
"", name,
schema.GroupVersionResource{Group: group, Version: version, Resource: resource}, subresource,
operation,
false,
nil,
)
}
func attrList(a ...admission.Attributes) []admission.Attributes {
return a
}
......@@ -299,3 +325,93 @@ func TestResource(t *testing.T) {
}
}
}
func TestScope(t *testing.T) {
cluster := adreg.ClusterScope
namespace := adreg.NamespacedScope
allscopes := adreg.AllScopes
table := tests{
"cluster scope": {
rule: adreg.RuleWithOperations{
Rule: adreg.Rule{
Resources: []string{"*"},
Scope: &cluster,
},
},
match: attrList(
clusterScopedAttributes("g", "v", "r", "", "name", admission.Create),
clusterScopedAttributes("g", "v", "r", "exec", "name", admission.Create),
clusterScopedAttributes("", "v1", "namespaces", "", "ns", admission.Create),
clusterScopedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create),
namespacedAttributes("", "v1", "namespaces", "", "ns", admission.Create),
namespacedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create),
),
noMatch: attrList(
namespacedAttributes("g", "v", "r", "", "name", admission.Create),
namespacedAttributes("g", "v", "r", "exec", "name", admission.Create),
),
},
"namespace scope": {
rule: adreg.RuleWithOperations{
Rule: adreg.Rule{
Resources: []string{"*"},
Scope: &namespace,
},
},
match: attrList(
namespacedAttributes("g", "v", "r", "", "name", admission.Create),
namespacedAttributes("g", "v", "r", "exec", "name", admission.Create),
),
noMatch: attrList(
clusterScopedAttributes("", "v1", "namespaces", "", "ns", admission.Create),
clusterScopedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create),
namespacedAttributes("", "v1", "namespaces", "", "ns", admission.Create),
namespacedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create),
clusterScopedAttributes("g", "v", "r", "", "name", admission.Create),
clusterScopedAttributes("g", "v", "r", "exec", "name", admission.Create),
),
},
"all scopes": {
rule: adreg.RuleWithOperations{
Rule: adreg.Rule{
Resources: []string{"*"},
Scope: &allscopes,
},
},
match: attrList(
namespacedAttributes("g", "v", "r", "", "name", admission.Create),
namespacedAttributes("g", "v", "r", "exec", "name", admission.Create),
clusterScopedAttributes("g", "v", "r", "", "name", admission.Create),
clusterScopedAttributes("g", "v", "r", "exec", "name", admission.Create),
clusterScopedAttributes("", "v1", "namespaces", "", "ns", admission.Create),
clusterScopedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create),
namespacedAttributes("", "v1", "namespaces", "", "ns", admission.Create),
namespacedAttributes("", "v1", "namespaces", "finalize", "ns", admission.Create),
),
noMatch: attrList(),
},
}
keys := sets.NewString()
for name := range table {
keys.Insert(name)
}
for _, name := range keys.List() {
tt := table[name]
for i, m := range tt.match {
t.Run(fmt.Sprintf("%s_match_%d", name, i), func(t *testing.T) {
r := Matcher{tt.rule, m}
if !r.scope() {
t.Errorf("%v: expected match %#v", name, m)
}
})
}
for i, m := range tt.noMatch {
t.Run(fmt.Sprintf("%s_nomatch_%d", name, i), func(t *testing.T) {
r := Matcher{tt.rule, m}
if r.scope() {
t.Errorf("%v: expected no match %#v", name, m)
}
})
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment