Skip to content
Permalink
Browse files

WIP - Filter namespace scoped resources by namespace label

  • Loading branch information...
smarterclayton committed Sep 11, 2019
1 parent b5dfac8 commit f38f683be2694f10a8d20218093d2d14d0c9f7f7

Some generated files are not rendered by default. Learn more.

@@ -33,6 +33,12 @@ type ListOptions struct {
LabelSelector labels.Selector
// A selector based on fields
FieldSelector fields.Selector
// A selector to restrict the set of namespaces that objects are returned from
// by the namespace label. Using this field with a resource that is not namespace
// scoped will return a bad request error. The default is to select all namespaces.
// +optional
NamespaceLabelSelector labels.Selector

// If true, watch for changes to this list
Watch bool
// allowWatchBookmarks requests watch events with type "BOOKMARK".

Some generated files are not rendered by default. Learn more.

Some generated files are not rendered by default. Learn more.

@@ -330,6 +330,12 @@ type ListOptions struct {
// +optional
FieldSelector string `json:"fieldSelector,omitempty" protobuf:"bytes,2,opt,name=fieldSelector"`

// A selector to restrict the set of namespaces that objects are returned from
// by the namespace label. Using this field with a resource that is not namespace
// scoped will return a bad request error. The default is to select all namespaces.
// +optional
NamespaceLabelSelector string `json:"namespaceLabelSelector,omitempty"`

// +k8s:deprecated=includeUninitialized,protobuf=6

// Watch for changes to the described resources and return them as a stream of
@@ -225,6 +225,12 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
return
}

if opts.NamespaceLabelSelector != nil && !opts.NamespaceLabelSelector.Empty() && !scope.NamespaceScoped {
err = errors.NewBadRequest("namespace label selection is only valid on namespace scoped resources")
scope.err(err, w, req)
return
}

// transform fields
// TODO: DecodeParametersInto should do this.
if opts.FieldSelector != nil {
@@ -69,9 +69,10 @@ type RequestScope struct {
TableConvertor rest.TableConvertor
FieldManager *fieldmanager.FieldManager

Resource schema.GroupVersionResource
Kind schema.GroupVersionKind
Subresource string
Resource schema.GroupVersionResource
Kind schema.GroupVersionKind
Subresource string
NamespaceScoped bool

MetaGroupVersion schema.GroupVersion

@@ -539,6 +539,8 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Resource: a.group.GroupVersion.WithResource(resource),
Subresource: subresource,
Kind: fqKindToRegister,

NamespaceScoped: namespaceScoped,

HubGroupVersion: schema.GroupVersion{Group: fqKindToRegister.Group, Version: runtime.APIVersionInternal},

@@ -19,6 +19,7 @@ package generic
import (
"time"

"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/storagebackend"
@@ -33,6 +34,8 @@ type RESTOptions struct {
DeleteCollectionWorkers int
ResourcePrefix string
CountMetricPollPeriod time.Duration

NamespaceLabelsAccessor func(ns string) (labels.Labels, error)
}

// Implement RESTOptionsGetter so that RESTOptions can directly be used when available (i.e. tests)
@@ -126,6 +126,11 @@ type Store struct {
// object matches the given field and label selectors.
PredicateFunc func(label labels.Selector, field fields.Selector) storage.SelectionPredicate

// NamespaceLabelsAccessor allows you to retrieve the labels for a given
// namespace. It returns an API error if the namespace does not exist, or
// a generic error if the namespace name is invalid.
NamespaceLabelsAccessor func(ns string) (labels.Labels, error)

// EnableGarbageCollection affects the handling of Update and Delete
// requests. Enabling garbage collection allows finalizers to do work to
// finalize this object before the store deletes it.
@@ -318,6 +323,22 @@ func (e *Store) ListPredicate(ctx context.Context, p storage.SelectionPredicate,
// By default we should serve the request from etcd.
options = &metainternalversion.ListOptions{ResourceVersion: ""}
}

if options.NamespaceLabelSelector != nil && !options.NamespaceLabelSelector.Empty() {
klog.Infof("DEBUG: namespace label selector %s", options.NamespaceLabelSelector)
if e.NamespaceLabelsAccessor == nil {
return nil, kubeerr.NewBadRequest(fmt.Sprintf("resource %s does not support namespace label selection", e.DefaultQualifiedResource.String()))
}
p.NamespaceMatch = func(ns string) bool {
labels, err := e.NamespaceLabelsAccessor(ns)
klog.Infof("DEBUG: check ns %s on query for %s: %s %v", ns, e.DefaultQualifiedResource.String(), labels, err)
if err != nil {
return false
}
return options.NamespaceLabelSelector.Matches(labels)
}
}

p.Limit = options.Limit
p.Continue = options.Continue
list := e.NewListFunc()
@@ -1095,6 +1116,17 @@ func (e *Store) Watch(ctx context.Context, options *metainternalversion.ListOpti
}
predicate := e.PredicateFunc(label, field)

if options.NamespaceLabelSelector != nil && e.NamespaceLabelsAccessor != nil {
predicate.NamespaceMatch = func(ns string) bool {
labels, err := e.NamespaceLabelsAccessor(ns)
klog.Infof("DEBUG: check ns %s on query for %s: %v", ns, e.DefaultQualifiedResource.String(), err)
if err != nil {
return false
}
return options.NamespaceLabelSelector.Matches(labels)
}
}

resourceVersion := ""
if options != nil {
resourceVersion = options.ResourceVersion
@@ -1244,6 +1276,10 @@ func (e *Store) CompleteWithOptions(options *generic.StoreOptions) error {
return err
}

if opts.NamespaceLabelsAccessor != nil {
e.NamespaceLabelsAccessor = opts.NamespaceLabelsAccessor
}

// ResourcePrefix must come from the underlying factory
prefix := opts.ResourcePrefix
if !strings.HasPrefix(prefix, "/") {
@@ -33,6 +33,7 @@ import (
"github.com/go-openapi/spec"
"github.com/pborman/uuid"

"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -61,6 +62,7 @@ import (
"k8s.io/apiserver/pkg/server/routes"
serverstore "k8s.io/apiserver/pkg/server/storage"
"k8s.io/client-go/informers"
corev1listers "k8s.io/client-go/listers/core/v1"
restclient "k8s.io/client-go/rest"
certutil "k8s.io/client-go/util/cert"
"k8s.io/component-base/logs"
@@ -380,13 +382,41 @@ type CompletedConfig struct {
*completedConfig
}

type namespaceAccessor struct {
getter genericregistry.RESTOptionsGetter
namespaceLister corev1listers.NamespaceLister
}

func (a namespaceAccessor) GetRESTOptions(resource schema.GroupResource) (genericregistry.RESTOptions, error) {
opt, err := a.getter.GetRESTOptions(resource)
if err != nil {
return opt, err
}
opt.NamespaceLabelsAccessor = func(name string) (labels.Labels, error) {
if len(name) == 0 {
return nil, fmt.Errorf("must pass a valid namespace to namespace labels lookup")
}
ns, err := a.namespaceLister.Get(name)
if err != nil {
return nil, err
}
return labels.Set(ns.Labels), nil
}
return opt, nil
}

// Complete fills in any fields not set that are required to have valid data and can be derived
// from other fields. If you're going to `ApplyOptions`, do that first. It's mutating the receiver.
func (c *Config) Complete(informers informers.SharedInformerFactory) CompletedConfig {
if len(c.ExternalAddress) == 0 && c.PublicAddress != nil {
c.ExternalAddress = c.PublicAddress.String()
}

c.RESTOptionsGetter = namespaceAccessor{
getter: c.RESTOptionsGetter,
namespaceLister: informers.Core().V1().Namespaces().Lister(),
}

// if there is no port, and we listen on one securely, use that one
if _, _, err := net.SplitHostPort(c.ExternalAddress); err != nil {
if c.SecureServing == nil {
@@ -997,7 +997,15 @@ func filterWithAttrsFunction(key string, p storage.SelectionPredicate) filterWit
if !hasPathPrefix(objKey, key) {
return false
}
return p.MatchesObjectAttributes(label, field)
if !p.MatchesObjectAttributes(label, field) {
return false
}
if p.NamespaceMatch != nil {
if ns := field.Get("metadata.namespace"); len(ns) > 0 && !p.NamespaceMatch(ns) {
return false
}
}
return true
}
return filterFunc
}
@@ -78,6 +78,8 @@ type SelectionPredicate struct {
Limit int64
Continue string
AllowWatchBookmarks bool

NamespaceMatch func(ns string) bool
}

// Matches returns true if the given object's labels and fields (as
@@ -93,7 +95,10 @@ func (s *SelectionPredicate) Matches(obj runtime.Object) (bool, error) {
}
matched := s.Label.Matches(labels)
if matched && s.Field != nil {
matched = matched && s.Field.Matches(fields)
matched = s.Field.Matches(fields)
}
if matched && s.NamespaceMatch != nil {
matched = s.NamespaceMatch(fields.Get("metadata.namespace"))
}
return matched, nil
}
@@ -126,5 +131,5 @@ func (s *SelectionPredicate) MatchesSingle() (string, bool) {

// Empty returns true if the predicate performs no filtering.
func (s *SelectionPredicate) Empty() bool {
return s.Label.Empty() && s.Field.Empty()
return s.Label.Empty() && s.Field.Empty() && s.NamespaceMatch == nil
}

Some generated files are not rendered by default. Learn more.

Some generated files are not rendered by default. Learn more.

0 comments on commit f38f683

Please sign in to comment.
You can’t perform that action at this time.