Skip to content

Commit

Permalink
Refactor NamespaceSelector to use GetMatches
Browse files Browse the repository at this point in the history
This reduces some duplicated logic, and provides another example for
users of how to implement the ResourceList interface.

Signed-off-by: Justin Kulikauskas <[email protected]>
  • Loading branch information
JustinKuli committed May 4, 2024
1 parent 34d6bd7 commit 03e5dc9
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 67 deletions.
49 changes: 49 additions & 0 deletions api/v1beta1/policycore_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
package v1beta1

import (
"context"
"encoding/json"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// NOTE: json tags are required. Any new fields you add must have json tags for
Expand Down Expand Up @@ -97,6 +100,52 @@ func (sel NamespaceSelector) MarshalJSON() ([]byte, error) {
}
}

// GetNamespaces fetches all namespaces in the cluster and returns a list of the
// namespaces that match the NamespaceSelector. The client.Reader needs access
// for viewing namespaces, like the access given by this kubebuilder tag:
// `//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch`
func (sel NamespaceSelector) GetNamespaces(ctx context.Context, r client.Reader) ([]string, error) {
if len(sel.Include) == 0 && sel.LabelSelector == nil {
// A somewhat special case of no matches.
return []string{}, nil
}

t := Target{
LabelSelector: sel.LabelSelector,
Include: sel.Include,
Exclude: sel.Exclude,
}

matchingNamespaces, err := t.GetMatches(ctx, r, &namespaceResList{})
if err != nil {
return nil, err
}

names := make([]string, len(matchingNamespaces))
for i, ns := range matchingNamespaces {
names[i] = ns.GetName()
}

return names, nil
}

type namespaceResList struct {
corev1.NamespaceList
}

func (l *namespaceResList) Items() ([]client.Object, error) {
items := make([]client.Object, len(l.NamespaceList.Items))
for i := range l.NamespaceList.Items {
items[i] = &l.NamespaceList.Items[i]
}

return items, nil
}

func (l *namespaceResList) ObjectList() client.ObjectList {
return &l.NamespaceList
}

//+kubebuilder:validation:MinLength=1

type NonEmptyString string
Expand Down
67 changes: 0 additions & 67 deletions api/v1beta1/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,12 @@ import (
"fmt"
"path/filepath"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// GetNamespaces fetches all namespaces in the cluster and returns a list of the
// namespaces that match the NamespaceSelector. The client.Reader needs access
// for viewing namespaces, like the access given by this kubebuilder tag:
// `//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch`
func (sel NamespaceSelector) GetNamespaces(ctx context.Context, r client.Reader) ([]string, error) {
if len(sel.Include) == 0 && sel.LabelSelector == nil {
// A somewhat special case of no matches.
return []string{}, nil
}

listOpts := client.ListOptions{}

if sel.LabelSelector != nil {
labelSel, err := metav1.LabelSelectorAsSelector(sel.LabelSelector)
if err != nil {
return nil, err
}

listOpts.LabelSelector = labelSel
}

namespaceList := &corev1.NamespaceList{}
if err := r.List(ctx, namespaceList, &listOpts); err != nil {
return nil, err
}

namespaces := make([]string, len(namespaceList.Items))
for i, ns := range namespaceList.Items {
namespaces[i] = ns.GetName()
}

t := Target{
LabelSelector: sel.LabelSelector,
Include: sel.Include,
Exclude: sel.Exclude,
}

return t.matches(namespaces)
}

type Target struct {
*metav1.LabelSelector `json:",inline"`

Expand Down Expand Up @@ -176,32 +135,6 @@ func (t Target) matchesByName(items []client.Object) ([]client.Object, error) {
return matches, nil
}

// matches filters a slice of strings, and returns ones that match the Include
// and Exclude lists in the Target. The only possible returned error is a
// wrapped filepath.ErrBadPattern.
func (t Target) matches(names []string) ([]string, error) {
// Using a map to ensure each entry in the result is unique.
set := make(map[string]struct{})

for _, name := range names {
matched, err := t.match(name)
if err != nil {
return nil, err
}

if matched {
set[name] = struct{}{}
}
}

matchingNames := make([]string, 0, len(set))
for ns := range set {
matchingNames = append(matchingNames, ns)
}

return matchingNames, nil
}

// match returns whether the given name matches the Include and Exclude lists in
// the Target.
func (t Target) match(name string) (bool, error) {
Expand Down
24 changes: 24 additions & 0 deletions api/v1beta1/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ var sampleNames = []string{
"foo", "bar", "baz", "boo", "default", "kube-one", "kube-two", "kube-three",
}

// matches is only used to unit-test the behavior of `match`
func (t *Target) matches(names []string) ([]string, error) {
// Using a map to ensure each entry in the result is unique.
set := make(map[string]struct{})

for _, name := range names {
matched, err := t.match(name)
if err != nil {
return nil, err
}

if matched {
set[name] = struct{}{}
}
}

matchingNames := make([]string, 0, len(set))
for ns := range set {
matchingNames = append(matchingNames, ns)
}

return matchingNames, nil
}

// Fuzz test to verify that excluding "*" always matches 0 names. The
// `Include` list and the input names are both fuzzed.
func FuzzMatchesExcludeAll(f *testing.F) {
Expand Down

0 comments on commit 03e5dc9

Please sign in to comment.