Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into webhook-warning
Browse files Browse the repository at this point in the history
  • Loading branch information
STRRL committed Nov 3, 2022
2 parents 86c19e8 + c2f04bb commit 469500b
Show file tree
Hide file tree
Showing 18 changed files with 561 additions and 49 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ in sig apimachinery.

You can reach the maintainers of this project at:

- Slack channel: [#kubebuilder](http://slack.k8s.io/#kubebuilder)
- Slack channel: [#controller-runtime](https://kubernetes.slack.com/archives/C02MRBMN00Z)
- Google Group: [kubebuilder@googlegroups.com](https://groups.google.com/forum/#!forum/kubebuilder)

## Contributing
Expand Down
2 changes: 1 addition & 1 deletion TMP-LOGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ log.Printf("starting reconciliation for pod %s/%s", podNamespace, podName)
In controller-runtime, we'd instead write:

```go
logger.Info("starting reconciliation", "pod", req.NamespacedNamed)
logger.Info("starting reconciliation", "pod", req.NamespacedName)
```

or even write
Expand Down
4 changes: 4 additions & 0 deletions alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ type TypeMeta = metav1.TypeMeta
type ObjectMeta = metav1.ObjectMeta

var (
// RegisterFlags registers flag variables to the given FlagSet if not already registered.
// It uses the default command line FlagSet, if none is provided. Currently, it only registers the kubeconfig flag.
RegisterFlags = config.RegisterFlags

// GetConfigOrDie creates a *rest.Config for talking to a Kubernetes apiserver.
// If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running
// in cluster and use the cluster provided kubeconfig.
Expand Down
18 changes: 2 additions & 16 deletions pkg/cache/internal/cache_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ import (

apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/internal/field/selector"

"sigs.k8s.io/controller-runtime/pkg/client"
)
Expand Down Expand Up @@ -116,7 +115,7 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
case listOpts.FieldSelector != nil:
// TODO(directxman12): support more complicated field selectors by
// combining multiple indices, GetIndexers, etc
field, val, requiresExact := requiresExactMatch(listOpts.FieldSelector)
field, val, requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
if !requiresExact {
return fmt.Errorf("non-exact field matches are not supported by the cache")
}
Expand Down Expand Up @@ -186,19 +185,6 @@ func objectKeyToStoreKey(k client.ObjectKey) string {
return k.Namespace + "/" + k.Name
}

// requiresExactMatch checks if the given field selector is of the form `k=v` or `k==v`.
func requiresExactMatch(sel fields.Selector) (field, val string, required bool) {
reqs := sel.Requirements()
if len(reqs) != 1 {
return "", "", false
}
req := reqs[0]
if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals {
return "", "", false
}
return req.Field, req.Value, true
}

// FieldIndexName constructs the name of the index over the given field,
// for use with an indexer.
func FieldIndexName(field string) string {
Expand Down
23 changes: 20 additions & 3 deletions pkg/client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,32 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
)

// KubeconfigFlagName is the name of the kubeconfig flag
const KubeconfigFlagName = "kubeconfig"

var (
kubeconfig string
log = logf.RuntimeLog.WithName("client").WithName("config")
)

// init registers the "kubeconfig" flag to the default command line FlagSet.
// TODO: This should be removed, as it potentially leads to redefined flag errors for users, if they already
// have registered the "kubeconfig" flag to the command line FlagSet in other parts of their code.
func init() {
// TODO: Fix this to allow double vendoring this library but still register flags on behalf of users
flag.StringVar(&kubeconfig, "kubeconfig", "",
"Paths to a kubeconfig. Only required if out-of-cluster.")
RegisterFlags(flag.CommandLine)
}

// RegisterFlags registers flag variables to the given FlagSet if not already registered.
// It uses the default command line FlagSet, if none is provided. Currently, it only registers the kubeconfig flag.
func RegisterFlags(fs *flag.FlagSet) {
if fs == nil {
fs = flag.CommandLine
}
if f := fs.Lookup(KubeconfigFlagName); f != nil {
kubeconfig = f.Value.String()
} else {
fs.StringVar(&kubeconfig, KubeconfigFlagName, "", "Paths to a kubeconfig. Only required if out-of-cluster.")
}
}

// GetConfig creates a *rest.Config for talking to a Kubernetes API server.
Expand Down
142 changes: 130 additions & 12 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/testing"
"sigs.k8s.io/controller-runtime/pkg/internal/field/selector"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
Expand All @@ -49,9 +52,14 @@ type versionedTracker struct {
}

type fakeClient struct {
tracker versionedTracker
scheme *runtime.Scheme
restMapper meta.RESTMapper
tracker versionedTracker
scheme *runtime.Scheme
restMapper meta.RESTMapper

// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
// The inner map maps from index name to IndexerFunc.
indexes map[schema.GroupVersionKind]map[string]client.IndexerFunc

schemeWriteLock sync.Mutex
}

Expand Down Expand Up @@ -93,6 +101,10 @@ type ClientBuilder struct {
initLists []client.ObjectList
initRuntimeObjects []runtime.Object
objectTracker testing.ObjectTracker

// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
// The inner map maps from index name to IndexerFunc.
indexes map[schema.GroupVersionKind]map[string]client.IndexerFunc
}

// WithScheme sets this builder's internal scheme.
Expand Down Expand Up @@ -135,6 +147,44 @@ func (f *ClientBuilder) WithObjectTracker(ot testing.ObjectTracker) *ClientBuild
return f
}

// WithIndex can be optionally used to register an index with name `field` and indexer `extractValue`
// for API objects of the same GroupVersionKind (GVK) as `obj` in the fake client.
// It can be invoked multiple times, both with objects of the same GVK or different ones.
// Invoking WithIndex twice with the same `field` and GVK (via `obj`) arguments will panic.
// WithIndex retrieves the GVK of `obj` using the scheme registered via WithScheme if
// WithScheme was previously invoked, the default scheme otherwise.
func (f *ClientBuilder) WithIndex(obj runtime.Object, field string, extractValue client.IndexerFunc) *ClientBuilder {
objScheme := f.scheme
if objScheme == nil {
objScheme = scheme.Scheme
}

gvk, err := apiutil.GVKForObject(obj, objScheme)
if err != nil {
panic(err)
}

// If this is the first index being registered, we initialize the map storing all the indexes.
if f.indexes == nil {
f.indexes = make(map[schema.GroupVersionKind]map[string]client.IndexerFunc)
}

// If this is the first index being registered for the GroupVersionKind of `obj`, we initialize
// the map storing the indexes for that GroupVersionKind.
if f.indexes[gvk] == nil {
f.indexes[gvk] = make(map[string]client.IndexerFunc)
}

if _, fieldAlreadyIndexed := f.indexes[gvk][field]; fieldAlreadyIndexed {
panic(fmt.Errorf("indexer conflict: field %s for GroupVersionKind %v is already indexed",
field, gvk))
}

f.indexes[gvk][field] = extractValue

return f
}

// Build builds and returns a new fake client.
func (f *ClientBuilder) Build() client.WithWatch {
if f.scheme == nil {
Expand Down Expand Up @@ -171,6 +221,7 @@ func (f *ClientBuilder) Build() client.WithWatch {
tracker: tracker,
scheme: f.scheme,
restMapper: f.restMapper,
indexes: f.indexes,
}
}

Expand Down Expand Up @@ -420,21 +471,88 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
return err
}

if listOpts.LabelSelector != nil {
objs, err := meta.ExtractList(obj)
if listOpts.LabelSelector == nil && listOpts.FieldSelector == nil {
return nil
}

// If we're here, either a label or field selector are specified (or both), so before we return
// the list we must filter it. If both selectors are set, they are ANDed.
objs, err := meta.ExtractList(obj)
if err != nil {
return err
}

filteredList, err := c.filterList(objs, gvk, listOpts.LabelSelector, listOpts.FieldSelector)
if err != nil {
return err
}

return meta.SetList(obj, filteredList)
}

func (c *fakeClient) filterList(list []runtime.Object, gvk schema.GroupVersionKind, ls labels.Selector, fs fields.Selector) ([]runtime.Object, error) {
// Filter the objects with the label selector
filteredList := list
if ls != nil {
objsFilteredByLabel, err := objectutil.FilterWithLabels(list, ls)
if err != nil {
return err
return nil, err
}
filteredObjs, err := objectutil.FilterWithLabels(objs, listOpts.LabelSelector)
filteredList = objsFilteredByLabel
}

// Filter the result of the previous pass with the field selector
if fs != nil {
objsFilteredByField, err := c.filterWithFields(filteredList, gvk, fs)
if err != nil {
return err
return nil, err
}
err = meta.SetList(obj, filteredObjs)
if err != nil {
return err
filteredList = objsFilteredByField
}

return filteredList, nil
}

func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVersionKind, fs fields.Selector) ([]runtime.Object, error) {
// We only allow filtering on the basis of a single field to ensure consistency with the
// behavior of the cache reader (which we're faking here).
fieldKey, fieldVal, requiresExact := selector.RequiresExactMatch(fs)
if !requiresExact {
return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"",
fs)
}

// Field selection is mimicked via indexes, so there's no sane answer this function can give
// if there are no indexes registered for the GroupVersionKind of the objects in the list.
indexes := c.indexes[gvk]
if len(indexes) == 0 || indexes[fieldKey] == nil {
return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+
"index with name %s has been registered for GroupVersionKind %v", gvk, fieldKey, fieldKey, gvk)
}

indexExtractor := indexes[fieldKey]
filteredList := make([]runtime.Object, 0, len(list))
for _, obj := range list {
if c.objMatchesFieldSelector(obj, indexExtractor, fieldVal) {
filteredList = append(filteredList, obj)
}
}
return nil
return filteredList, nil
}

func (c *fakeClient) objMatchesFieldSelector(o runtime.Object, extractIndex client.IndexerFunc, val string) bool {
obj, isClientObject := o.(client.Object)
if !isClientObject {
panic(fmt.Errorf("expected object %v to be of type client.Object, but it's not", o))
}

for _, extractedVal := range extractIndex(obj) {
if extractedVal == val {
return true
}
}

return false
}

func (c *fakeClient) Scheme() *runtime.Scheme {
Expand Down

0 comments on commit 469500b

Please sign in to comment.