Bump sigs.k8s.io/controller-runtime to v0.14.4 (#5507)

* Bump sigs.k8s.io/controller-runtime to v0.14.4

* Update gofmt
This commit is contained in:
hongming
2023-02-08 14:06:15 +08:00
committed by GitHub
parent 129e6fbec3
commit 1c49fcd57e
1404 changed files with 141422 additions and 47769 deletions

View File

@@ -22,3 +22,6 @@
# Tools binaries.
hack/tools/bin
junit-report.xml
/artifacts

View File

@@ -3,7 +3,6 @@ linters:
enable:
- asciicheck
- bodyclose
- deadcode
- depguard
- dogsled
- errcheck
@@ -12,14 +11,12 @@ linters:
- goconst
- gocritic
- gocyclo
- godot
- gofmt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- ifshort
- importas
- ineffassign
- misspell
@@ -30,19 +27,14 @@ linters:
- revive
- rowserrcheck
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
linters-settings:
ifshort:
# Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax.
max-decl-chars: 50
importas:
no-unaliased: true
alias:
@@ -61,9 +53,9 @@ linters-settings:
- pkg: sigs.k8s.io/controller-runtime
alias: ctrl
staticcheck:
go: "1.18"
go: "1.19"
stylecheck:
go: "1.18"
go: "1.19"
depguard:
include-go-root: true
packages:
@@ -132,6 +124,9 @@ issues:
- linters:
- gosec
text: "G304: Potential file inclusion via variable"
- linters:
- revive
text: "package-comments: should have a package comment"
run:
timeout: 10m

View File

@@ -4,9 +4,8 @@ aliases:
# active folks who can be contacted to perform admin-related
# tasks on the repo, or otherwise approve any PRS.
controller-runtime-admins:
- droot
- mengqiy
- pwittrock
- vincepri
- joelanford
# non-admin folks who have write-access and can approve any PRs in the repo
controller-runtime-maintainers:
@@ -15,19 +14,18 @@ aliases:
# non-admin folks who can approve any PRs in the repo
controller-runtime-approvers:
- gerred
- shawn-hurley
- alvaroaleman
- fillzpp
- sbueringer
# folks who can review and LGTM any PRs in the repo (doesn't
# include approvers & admins -- those count too via the OWNERS
# file)
controller-runtime-reviewers:
- alenkacz
- vincepri
- alexeldeib
- varshaprasad96
- fillzpp
- sbueringer
# folks to can approve things in the directly-ported
# testing_frameworks portions of the codebase
@@ -39,3 +37,7 @@ aliases:
# but are no longer directly involved
controller-runtime-emeritus-maintainers:
- directxman12
controller-runtime-emeritus-admins:
- droot
- mengqiy
- pwittrock

View File

@@ -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

47
vendor/sigs.k8s.io/controller-runtime/RELEASE.md generated vendored Normal file
View File

@@ -0,0 +1,47 @@
# Release Process
The Kubernetes controller-runtime Project is released on an as-needed basis. The process is as follows:
**Note:** Releases are done from the `release-MAJOR.MINOR` branches. For PATCH releases is not required
to create a new branch you will just need to ensure that all big fixes are cherry-picked into the respective
`release-MAJOR.MINOR` branch. To know more about versioning check https://semver.org/.
## How to do a release
### Create the new branch and the release tag
1. Create a new branch `git checkout -b release-<MAJOR.MINOR>` from master
2. Push the new branch to the remote repository
### Now, let's generate the changelog
1. Create the changelog from the new branch `release-<MAJOR.MINOR>` (`git checkout release-<MAJOR.MINOR>`).
You will need to use the [kubebuilder-release-tools][kubebuilder-release-tools] to generate the notes. See [here][release-notes-generation]
> **Note**
> - You will need to have checkout locally from the remote repository the previous branch
> - Also, ensure that you fetch all tags from the remote `git fetch --all --tags`
### Draft a new release from GitHub
1. Create a new tag with the correct version from the new `release-<MAJOR.MINOR>` branch
2. Add the changelog on it and publish. Now, the code source is released !
### Add a new Prow test the for the new branch release
1. Create a new prow test under [github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/controller-runtime](https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/controller-runtime)
for the new `release-<MAJOR.MINOR>` branch. (i.e. for the `0.11.0` release see the PR: https://github.com/kubernetes/test-infra/pull/25205)
2. Ping the infra PR in the controller-runtime slack channel for reviews.
### Announce the new release:
1. Publish on the Slack channel the new release, i.e:
````
:announce: Controller-Runtime v0.12.0 has been released!
This release includes a Kubernetes dependency bump to v1.24.
For more info, see the release page: https://github.com/kubernetes-sigs/controller-runtime/releases.
:tada: Thanks to all our contributors!
````
2. An announcement email is sent to `kubebuilder@googlegroups.com` with the subject `[ANNOUNCE] Controller-Runtime $VERSION is released`

View File

@@ -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
@@ -51,7 +51,7 @@ You can configure the logging implementation using
`"sigs.k8s.io/controller-runtime/pkg/log".SetLogger`. That
package also contains the convenience functions for setting up Zap.
You can get a handle to the the "root" logger using
You can get a handle to the "root" logger using
`"sigs.k8s.io/controller-runtime/pkg/log".Log`, and can then call
`WithName` to create individual named loggers. You can call `WithName`
repeatedly to chain names together:

View File

@@ -21,7 +21,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client/config"
cfg "sigs.k8s.io/controller-runtime/pkg/config"
cfg "sigs.k8s.io/controller-runtime/pkg/config" //nolint:staticcheck
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
@@ -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.
@@ -95,7 +99,8 @@ var (
// ConfigFile returns the cfg.File function for deferred config file loading,
// this is passed into Options{}.From() to populate the Options fields for
// the manager.
ConfigFile = cfg.File
// Deprecated: This is deprecated in favor of using Options directly.
ConfigFile = cfg.File //nolint:staticcheck
// NewControllerManagedBy returns a new controller builder that will be started by the provided Manager.
NewControllerManagedBy = builder.ControllerManagedBy

View File

@@ -23,13 +23,14 @@ limitations under the License.
// and uncommon cases should be possible. In general, controller-runtime tries
// to guide users towards Kubernetes controller best-practices.
//
// Getting Started
// # Getting Started
//
// The main entrypoint for controller-runtime is this root package, which
// contains all of the common types needed to get started building controllers:
// import (
// ctrl "sigs.k8s.io/controller-runtime"
// )
//
// import (
// ctrl "sigs.k8s.io/controller-runtime"
// )
//
// The examples in this package walk through a basic controller setup. The
// kubebuilder book (https://book.kubebuilder.io) has some more in-depth
@@ -38,7 +39,7 @@ limitations under the License.
// controller-runtime favors structs with sane defaults over constructors, so
// it's fairly common to see structs being used directly in controller-runtime.
//
// Organization
// # Organization
//
// A brief-ish walkthrough of the layout of this library can be found below. Each
// package contains more information about how to use it.
@@ -47,7 +48,7 @@ limitations under the License.
// controllers can be found at
// https://github.com/kubernetes-sigs/controller-runtime/blob/master/FAQ.md.
//
// Managers
// # Managers
//
// Every controller and webhook is ultimately run by a Manager (pkg/manager). A
// manager is responsible for running controllers and webhooks, and setting up
@@ -56,7 +57,7 @@ limitations under the License.
// generally configured to gracefully shut down controllers on pod termination
// by wiring up a signal handler (pkg/manager/signals).
//
// Controllers
// # Controllers
//
// Controllers (pkg/controller) use events (pkg/event) to eventually trigger
// reconcile requests. They may be constructed manually, but are often
@@ -67,7 +68,7 @@ limitations under the License.
// trigger reconciles. There are pre-written utilities for the common cases, and
// interfaces and helpers for advanced cases.
//
// Reconcilers
// # Reconcilers
//
// Controller logic is implemented in terms of Reconcilers (pkg/reconcile). A
// Reconciler implements a function which takes a reconcile Request containing
@@ -75,7 +76,7 @@ limitations under the License.
// and returns a Response or an error indicating whether to requeue for a
// second round of processing.
//
// Clients and Caches
// # Clients and Caches
//
// Reconcilers use Clients (pkg/client) to access API objects. The default
// client provided by the manager reads from a local shared cache (pkg/cache)
@@ -91,19 +92,19 @@ limitations under the License.
// may retrieve event recorders (pkg/recorder) to emit events using the
// manager.
//
// Schemes
// # Schemes
//
// Clients, Caches, and many other things in Kubernetes use Schemes
// (pkg/scheme) to associate Go types to Kubernetes API Kinds
// (Group-Version-Kinds, to be specific).
//
// Webhooks
// # Webhooks
//
// Similarly, webhooks (pkg/webhook/admission) may be implemented directly, but
// are often constructed using a builder (pkg/webhook/admission/builder). They
// are run via a server (pkg/webhook) which is managed by a Manager.
//
// Logging and Metrics
// # Logging and Metrics
//
// Logging (pkg/log) in controller-runtime is done via structured logs, using a
// log set of interfaces called logr
@@ -117,7 +118,7 @@ limitations under the License.
// serve these by an HTTP endpoint, and additional metrics may be registered to
// this Registry as normal.
//
// Testing
// # Testing
//
// You can easily build integration and unit tests for your controllers and
// webhooks using the test Environment (pkg/envtest). This will automatically

View File

@@ -17,6 +17,7 @@ limitations under the License.
package builder
import (
"errors"
"fmt"
"strings"
@@ -182,10 +183,6 @@ func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, erro
if blder.forInput.err != nil {
return nil, blder.forInput.err
}
// Checking the reconcile type exist or not
if blder.forInput.object == nil {
return nil, fmt.Errorf("must provide an object for reconciliation")
}
// Set the ControllerManagedBy
if err := blder.doController(r); err != nil {
@@ -219,18 +216,23 @@ func (blder *Builder) project(obj client.Object, proj objectProjection) (client.
func (blder *Builder) doWatch() error {
// Reconcile type
typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
if err != nil {
return err
}
src := &source.Kind{Type: typeForSrc}
hdler := &handler.EnqueueRequestForObject{}
allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
return err
if blder.forInput.object != nil {
typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
if err != nil {
return err
}
src := &source.Kind{Type: typeForSrc}
hdler := &handler.EnqueueRequestForObject{}
allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
return err
}
}
// Watches the managed types
if len(blder.ownsInput) > 0 && blder.forInput.object == nil {
return errors.New("Owns() can only be used together with For()")
}
for _, own := range blder.ownsInput {
typeForSrc, err := blder.project(own.object, own.objectProjection)
if err != nil {
@@ -249,6 +251,9 @@ func (blder *Builder) doWatch() error {
}
// Do the watch requests
if len(blder.watchesInput) == 0 && blder.forInput.object == nil {
return errors.New("there are no watches configured, controller will never get triggered. Use For(), Owns() or Watches() to set them up")
}
for _, w := range blder.watchesInput {
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
allPredicates = append(allPredicates, w.predicates...)
@@ -269,15 +274,18 @@ func (blder *Builder) doWatch() error {
return nil
}
func (blder *Builder) getControllerName(gvk schema.GroupVersionKind) string {
func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
if blder.name != "" {
return blder.name
return blder.name, nil
}
return strings.ToLower(gvk.Kind)
if !hasGVK {
return "", errors.New("one of For() or Named() must be called")
}
return strings.ToLower(gvk.Kind), nil
}
func (blder *Builder) doController(r reconcile.Reconciler) error {
globalOpts := blder.mgr.GetControllerOptions()
globalOpts := blder.mgr.GetControllerOptions() //nolint:staticcheck
ctrlOptions := blder.ctrlOptions
if ctrlOptions.Reconciler == nil {
@@ -286,13 +294,18 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
// Retrieve the GVK from the object we're reconciling
// to prepopulate logger information, and to optionally generate a default name.
gvk, err := getGvk(blder.forInput.object, blder.mgr.GetScheme())
if err != nil {
return err
var gvk schema.GroupVersionKind
hasGVK := blder.forInput.object != nil
if hasGVK {
var err error
gvk, err = getGvk(blder.forInput.object, blder.mgr.GetScheme())
if err != nil {
return err
}
}
// Setup concurrency.
if ctrlOptions.MaxConcurrentReconciles == 0 {
if ctrlOptions.MaxConcurrentReconciles == 0 && hasGVK {
groupKind := gvk.GroupKind().String()
if concurrency, ok := globalOpts.GroupKindConcurrency[groupKind]; ok && concurrency > 0 {
@@ -305,23 +318,30 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
ctrlOptions.CacheSyncTimeout = *globalOpts.CacheSyncTimeout
}
controllerName := blder.getControllerName(gvk)
controllerName, err := blder.getControllerName(gvk, hasGVK)
if err != nil {
return err
}
// Setup the logger.
if ctrlOptions.LogConstructor == nil {
log = blder.mgr.GetLogger().WithValues(
log := blder.mgr.GetLogger().WithValues(
"controller", controllerName,
"controllerGroup", gvk.Group,
"controllerKind", gvk.Kind,
)
lowerCamelCaseKind := strings.ToLower(gvk.Kind[:1]) + gvk.Kind[1:]
if hasGVK {
log = log.WithValues(
"controllerGroup", gvk.Group,
"controllerKind", gvk.Kind,
)
}
ctrlOptions.LogConstructor = func(req *reconcile.Request) logr.Logger {
log := log
if req != nil {
if hasGVK {
log = log.WithValues(gvk.Kind, klog.KRef(req.Namespace, req.Name))
}
log = log.WithValues(
lowerCamelCaseKind, klog.KRef(req.Namespace, req.Name),
"namespace", req.Namespace, "name", req.Name,
)
}

View File

@@ -101,9 +101,9 @@ func (p projectAs) ApplyToWatches(opts *WatchesInput) {
var (
// OnlyMetadata tells the controller to *only* cache metadata, and to watch
// the the API server in metadata-only form. This is useful when watching
// the API server in metadata-only form. This is useful when watching
// lots of objects, really big objects, or objects for which you only know
// the the GVK, but not the structure. You'll need to pass
// the GVK, but not the structure. You'll need to pass
// metav1.PartialObjectMetadata to the client when fetching objects in your
// reconciler, otherwise you'll end up with a duplicate structured or
// unstructured cache.

View File

@@ -39,6 +39,7 @@ type WebhookBuilder struct {
gvk schema.GroupVersionKind
mgr manager.Manager
config *rest.Config
recoverPanic bool
}
// WebhookManagedBy allows inform its manager.Manager.
@@ -68,6 +69,12 @@ func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator)
return blder
}
// RecoverPanic indicates whether the panic caused by webhook should be recovered.
func (blder *WebhookBuilder) RecoverPanic() *WebhookBuilder {
blder.recoverPanic = true
return blder
}
// Complete builds the webhook.
func (blder *WebhookBuilder) Complete() error {
// Set the Config
@@ -124,10 +131,10 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() {
func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook {
if defaulter := blder.withDefaulter; defaulter != nil {
return admission.WithCustomDefaulter(blder.apiType, defaulter)
return admission.WithCustomDefaulter(blder.apiType, defaulter).WithRecoverPanic(blder.recoverPanic)
}
if defaulter, ok := blder.apiType.(admission.Defaulter); ok {
return admission.DefaultingWebhookFor(defaulter)
return admission.DefaultingWebhookFor(defaulter).WithRecoverPanic(blder.recoverPanic)
}
log.Info(
"skip registering a mutating webhook, object does not implement admission.Defaulter or WithDefaulter wasn't called",
@@ -153,10 +160,10 @@ func (blder *WebhookBuilder) registerValidatingWebhook() {
func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook {
if validator := blder.withValidator; validator != nil {
return admission.WithCustomValidator(blder.apiType, validator)
return admission.WithCustomValidator(blder.apiType, validator).WithRecoverPanic(blder.recoverPanic)
}
if validator, ok := blder.apiType.(admission.Validator); ok {
return admission.ValidatingWebhookFor(validator)
return admission.ValidatingWebhookFor(validator).WithRecoverPanic(blder.recoverPanic)
}
log.Info(
"skip registering a validating webhook, object does not implement admission.Validator or WithValidator wasn't called",

View File

@@ -19,14 +19,18 @@ package cache
import (
"context"
"fmt"
"reflect"
"time"
"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/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
toolscache "k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/cache/internal"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -74,11 +78,19 @@ type Informer interface {
// AddEventHandler adds an event handler to the shared informer using the shared informer's resync
// period. Events to a single handler are delivered sequentially, but there is no coordination
// between different handlers.
AddEventHandler(handler toolscache.ResourceEventHandler)
// It returns a registration handle for the handler that can be used to remove
// the handler again.
AddEventHandler(handler toolscache.ResourceEventHandler) (toolscache.ResourceEventHandlerRegistration, error)
// AddEventHandlerWithResyncPeriod adds an event handler to the shared informer using the
// specified resync period. Events to a single handler are delivered sequentially, but there is
// no coordination between different handlers.
AddEventHandlerWithResyncPeriod(handler toolscache.ResourceEventHandler, resyncPeriod time.Duration)
// It returns a registration handle for the handler that can be used to remove
// the handler again and an error if the handler cannot be added.
AddEventHandlerWithResyncPeriod(handler toolscache.ResourceEventHandler, resyncPeriod time.Duration) (toolscache.ResourceEventHandlerRegistration, error)
// RemoveEventHandler removes a formerly added event handler given by
// its registration handle.
// This function is guaranteed to be idempotent, and thread-safe.
RemoveEventHandler(handle toolscache.ResourceEventHandlerRegistration) error
// AddIndexers adds more indexers to this store. If you call this after you already have data
// in the store, the results are undefined.
AddIndexers(indexers toolscache.Indexers) error
@@ -150,7 +162,7 @@ func New(config *rest.Config, opts Options) (Cache, error) {
if err != nil {
return nil, err
}
selectorsByGVK, err := convertToSelectorsByGVK(opts.SelectorsByObject, opts.DefaultSelector, opts.Scheme)
selectorsByGVK, err := convertToByGVK(opts.SelectorsByObject, opts.DefaultSelector, opts.Scheme)
if err != nil {
return nil, err
}
@@ -158,41 +170,234 @@ func New(config *rest.Config, opts Options) (Cache, error) {
if err != nil {
return nil, err
}
transformByGVK, err := convertToTransformByKindAndGVK(opts.TransformByObject, opts.DefaultTransform, opts.Scheme)
transformByGVK, err := convertToByGVK(opts.TransformByObject, opts.DefaultTransform, opts.Scheme)
if err != nil {
return nil, err
}
transformByObj := internal.TransformFuncByObjectFromMap(transformByGVK)
internalSelectorsByGVK := internal.SelectorsByGVK{}
for gvk, selector := range selectorsByGVK {
internalSelectorsByGVK[gvk] = internal.Selector(selector)
}
im := internal.NewInformersMap(config, opts.Scheme, opts.Mapper, *opts.Resync, opts.Namespace, internalSelectorsByGVK, disableDeepCopyByGVK, transformByObj)
return &informerCache{InformersMap: im}, nil
}
// BuilderWithOptions returns a Cache constructor that will build a cache
// honoring the options argument, this is useful to specify options like
// SelectorsByObject
// WARNING: If SelectorsByObject is specified, filtered out resources are not
// returned.
// WARNING: If UnsafeDisableDeepCopy is enabled, you must DeepCopy any object
// returned from cache get/list before mutating it.
func BuilderWithOptions(options Options) NewCacheFunc {
return func(config *rest.Config, inherited Options) (Cache, error) {
var err error
inherited, err = defaultOpts(config, inherited)
if err != nil {
return nil, err
}
options, err = defaultOpts(config, options)
if err != nil {
return nil, err
}
combined, err := options.inheritFrom(inherited)
if err != nil {
return nil, err
}
return New(config, *combined)
}
}
func (options Options) inheritFrom(inherited Options) (*Options, error) {
var (
combined Options
err error
)
combined.Scheme = combineScheme(inherited.Scheme, options.Scheme)
combined.Mapper = selectMapper(inherited.Mapper, options.Mapper)
combined.Resync = selectResync(inherited.Resync, options.Resync)
combined.Namespace = selectNamespace(inherited.Namespace, options.Namespace)
combined.SelectorsByObject, combined.DefaultSelector, err = combineSelectors(inherited, options, combined.Scheme)
if err != nil {
return nil, err
}
combined.UnsafeDisableDeepCopyByObject, err = combineUnsafeDeepCopy(inherited, options, combined.Scheme)
if err != nil {
return nil, err
}
combined.TransformByObject, combined.DefaultTransform, err = combineTransforms(inherited, options, combined.Scheme)
if err != nil {
return nil, err
}
return &combined, nil
}
func combineScheme(schemes ...*runtime.Scheme) *runtime.Scheme {
var out *runtime.Scheme
for _, sch := range schemes {
if sch == nil {
continue
}
for gvk, t := range sch.AllKnownTypes() {
if out == nil {
out = runtime.NewScheme()
}
out.AddKnownTypeWithName(gvk, reflect.New(t).Interface().(runtime.Object))
}
}
return out
}
func selectMapper(def, override meta.RESTMapper) meta.RESTMapper {
if override != nil {
return override
}
return def
}
func selectResync(def, override *time.Duration) *time.Duration {
if override != nil {
return override
}
return def
}
func selectNamespace(def, override string) string {
if override != "" {
return override
}
return def
}
func combineSelectors(inherited, options Options, scheme *runtime.Scheme) (SelectorsByObject, ObjectSelector, error) {
// Selectors are combined via logical AND.
// - Combined label selector is a union of the selectors requirements from both sets of options.
// - Combined field selector uses fields.AndSelectors with the combined list of non-nil field selectors
// defined in both sets of options.
//
// There is a bunch of complexity here because we need to convert to SelectorsByGVK
// to be able to match keys between options and inherited and then convert back to SelectorsByObject
optionsSelectorsByGVK, err := convertToByGVK(options.SelectorsByObject, options.DefaultSelector, scheme)
if err != nil {
return nil, ObjectSelector{}, err
}
inheritedSelectorsByGVK, err := convertToByGVK(inherited.SelectorsByObject, inherited.DefaultSelector, inherited.Scheme)
if err != nil {
return nil, ObjectSelector{}, err
}
for gvk, inheritedSelector := range inheritedSelectorsByGVK {
optionsSelectorsByGVK[gvk] = combineSelector(inheritedSelector, optionsSelectorsByGVK[gvk])
}
return convertToByObject(optionsSelectorsByGVK, scheme)
}
func combineSelector(selectors ...ObjectSelector) ObjectSelector {
ls := make([]labels.Selector, 0, len(selectors))
fs := make([]fields.Selector, 0, len(selectors))
for _, s := range selectors {
ls = append(ls, s.Label)
fs = append(fs, s.Field)
}
return ObjectSelector{
Label: combineLabelSelectors(ls...),
Field: combineFieldSelectors(fs...),
}
}
func combineLabelSelectors(ls ...labels.Selector) labels.Selector {
var combined labels.Selector
for _, l := range ls {
if l == nil {
continue
}
if combined == nil {
combined = labels.NewSelector()
}
reqs, _ := l.Requirements()
combined = combined.Add(reqs...)
}
return combined
}
func combineFieldSelectors(fs ...fields.Selector) fields.Selector {
nonNil := fs[:0]
for _, f := range fs {
if f == nil {
continue
}
nonNil = append(nonNil, f)
}
if len(nonNil) == 0 {
return nil
}
if len(nonNil) == 1 {
return nonNil[0]
}
return fields.AndSelectors(nonNil...)
}
func combineUnsafeDeepCopy(inherited, options Options, scheme *runtime.Scheme) (DisableDeepCopyByObject, error) {
// UnsafeDisableDeepCopyByObject is combined via precedence. Only if a value for a particular GVK is unset
// in options will a value from inherited be used.
optionsDisableDeepCopyByGVK, err := convertToDisableDeepCopyByGVK(options.UnsafeDisableDeepCopyByObject, options.Scheme)
if err != nil {
return nil, err
}
inheritedDisableDeepCopyByGVK, err := convertToDisableDeepCopyByGVK(inherited.UnsafeDisableDeepCopyByObject, inherited.Scheme)
if err != nil {
return nil, err
}
im := internal.NewInformersMap(config, opts.Scheme, opts.Mapper, *opts.Resync, opts.Namespace, selectorsByGVK, disableDeepCopyByGVK, transformByGVK)
return &informerCache{InformersMap: im}, nil
for gvk, inheritedDeepCopy := range inheritedDisableDeepCopyByGVK {
if _, ok := optionsDisableDeepCopyByGVK[gvk]; !ok {
if optionsDisableDeepCopyByGVK == nil {
optionsDisableDeepCopyByGVK = map[schema.GroupVersionKind]bool{}
}
optionsDisableDeepCopyByGVK[gvk] = inheritedDeepCopy
}
}
return convertToDisableDeepCopyByObject(optionsDisableDeepCopyByGVK, scheme)
}
// BuilderWithOptions returns a Cache constructor that will build the a cache
// honoring the options argument, this is useful to specify options like
// SelectorsByObject
// WARNING: if SelectorsByObject is specified. filtered out resources are not
// returned.
// WARNING: if UnsafeDisableDeepCopy is enabled, you must DeepCopy any object
// returned from cache get/list before mutating it.
func BuilderWithOptions(options Options) NewCacheFunc {
return func(config *rest.Config, opts Options) (Cache, error) {
if options.Scheme == nil {
options.Scheme = opts.Scheme
}
if options.Mapper == nil {
options.Mapper = opts.Mapper
}
if options.Resync == nil {
options.Resync = opts.Resync
}
if options.Namespace == "" {
options.Namespace = opts.Namespace
}
if opts.Resync == nil {
opts.Resync = options.Resync
}
func combineTransforms(inherited, options Options, scheme *runtime.Scheme) (TransformByObject, toolscache.TransformFunc, error) {
// Transform functions are combined via chaining. If both inherited and options define a transform
// function, the transform function from inherited will be called first, and the transform function from
// options will be called second.
optionsTransformByGVK, err := convertToByGVK(options.TransformByObject, options.DefaultTransform, options.Scheme)
if err != nil {
return nil, nil, err
}
inheritedTransformByGVK, err := convertToByGVK(inherited.TransformByObject, inherited.DefaultTransform, inherited.Scheme)
if err != nil {
return nil, nil, err
}
return New(config, options)
for gvk, inheritedTransform := range inheritedTransformByGVK {
if optionsTransformByGVK == nil {
optionsTransformByGVK = map[schema.GroupVersionKind]toolscache.TransformFunc{}
}
optionsTransformByGVK[gvk] = combineTransform(inheritedTransform, optionsTransformByGVK[gvk])
}
return convertToByObject(optionsTransformByGVK, scheme)
}
func combineTransform(inherited, current toolscache.TransformFunc) toolscache.TransformFunc {
if inherited == nil {
return current
}
if current == nil {
return inherited
}
return func(in interface{}) (interface{}, error) {
mid, err := inherited(in)
if err != nil {
return nil, err
}
return current(mid)
}
}
@@ -219,17 +424,40 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
return opts, nil
}
func convertToSelectorsByGVK(selectorsByObject SelectorsByObject, defaultSelector ObjectSelector, scheme *runtime.Scheme) (internal.SelectorsByGVK, error) {
selectorsByGVK := internal.SelectorsByGVK{}
for object, selector := range selectorsByObject {
func convertToByGVK[T any](byObject map[client.Object]T, def T, scheme *runtime.Scheme) (map[schema.GroupVersionKind]T, error) {
byGVK := map[schema.GroupVersionKind]T{}
for object, value := range byObject {
gvk, err := apiutil.GVKForObject(object, scheme)
if err != nil {
return nil, err
}
selectorsByGVK[gvk] = internal.Selector(selector)
byGVK[gvk] = value
}
selectorsByGVK[schema.GroupVersionKind{}] = internal.Selector(defaultSelector)
return selectorsByGVK, nil
byGVK[schema.GroupVersionKind{}] = def
return byGVK, nil
}
func convertToByObject[T any](byGVK map[schema.GroupVersionKind]T, scheme *runtime.Scheme) (map[client.Object]T, T, error) {
var byObject map[client.Object]T
def := byGVK[schema.GroupVersionKind{}]
for gvk, value := range byGVK {
if gvk == (schema.GroupVersionKind{}) {
continue
}
obj, err := scheme.New(gvk)
if err != nil {
return nil, def, err
}
cObj, ok := obj.(client.Object)
if !ok {
return nil, def, fmt.Errorf("object %T for GVK %q does not implement client.Object", obj, gvk)
}
if byObject == nil {
byObject = map[client.Object]T{}
}
byObject[cObj] = value
}
return byObject, def, nil
}
// DisableDeepCopyByObject associate a client.Object's GVK to disable DeepCopy during get or list from cache.
@@ -259,17 +487,30 @@ func convertToDisableDeepCopyByGVK(disableDeepCopyByObject DisableDeepCopyByObje
return disableDeepCopyByGVK, nil
}
func convertToDisableDeepCopyByObject(byGVK internal.DisableDeepCopyByGVK, scheme *runtime.Scheme) (DisableDeepCopyByObject, error) {
var byObject DisableDeepCopyByObject
for gvk, value := range byGVK {
if byObject == nil {
byObject = DisableDeepCopyByObject{}
}
if gvk == (schema.GroupVersionKind{}) {
byObject[ObjectAll{}] = value
continue
}
obj, err := scheme.New(gvk)
if err != nil {
return nil, err
}
cObj, ok := obj.(client.Object)
if !ok {
return nil, fmt.Errorf("object %T for GVK %q does not implement client.Object", obj, gvk)
}
byObject[cObj] = value
}
return byObject, nil
}
// TransformByObject associate a client.Object's GVK to a transformer function
// to be applied when storing the object into the cache.
type TransformByObject map[client.Object]toolscache.TransformFunc
func convertToTransformByKindAndGVK(t TransformByObject, defaultTransform toolscache.TransformFunc, scheme *runtime.Scheme) (internal.TransformFuncByObject, error) {
result := internal.NewTransformFuncByObject()
for obj, transformation := range t {
if err := result.Set(obj, scheme, transformation); err != nil {
return nil, err
}
}
result.SetDefault(defaultTransform)
return result, nil
}

View File

@@ -51,7 +51,7 @@ type informerCache struct {
}
// Get implements Reader.
func (ip *informerCache) Get(ctx context.Context, key client.ObjectKey, out client.Object) error {
func (ip *informerCache) Get(ctx context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error {
gvk, err := apiutil.GVKForObject(out, ip.Scheme)
if err != nil {
return err

View File

@@ -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"
)
@@ -54,7 +53,7 @@ type CacheReader struct {
}
// Get checks the indexer for the object and writes a copy of it if found.
func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object) error {
func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error {
if c.scopeName == apimeta.RESTScopeNameRoot {
key.Namespace = ""
}
@@ -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")
}
@@ -162,7 +161,7 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
}
var outObj runtime.Object
if c.disableDeepCopy {
if c.disableDeepCopy || (listOpts.UnsafeDisableDeepCopy != nil && *listOpts.UnsafeDisableDeepCopy) {
// skip deep copy which might be unsafe
// you must DeepCopy any object before mutating it outside
outObj = obj
@@ -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 {

View File

@@ -4,6 +4,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)
@@ -20,12 +21,16 @@ type transformFuncByGVK struct {
transformers map[schema.GroupVersionKind]cache.TransformFunc
}
// NewTransformFuncByObject creates a new TransformFuncByObject instance.
func NewTransformFuncByObject() TransformFuncByObject {
return &transformFuncByGVK{
transformers: make(map[schema.GroupVersionKind]cache.TransformFunc),
defaultTransform: nil,
// TransformFuncByObjectFromMap creates a TransformFuncByObject from a map that
// maps GVKs to TransformFuncs.
func TransformFuncByObjectFromMap(in map[schema.GroupVersionKind]cache.TransformFunc) TransformFuncByObject {
byGVK := &transformFuncByGVK{}
if defaultFunc, hasDefault := in[schema.GroupVersionKind{}]; hasDefault {
byGVK.defaultTransform = defaultFunc
}
delete(in, schema.GroupVersionKind{})
byGVK.transformers = in
return byGVK
}
func (t *transformFuncByGVK) SetDefault(transformer cache.TransformFunc) {

View File

@@ -200,7 +200,7 @@ func (c *multiNamespaceCache) IndexField(ctx context.Context, obj client.Object,
return nil
}
func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error {
func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
isNamespaced, err := objectutil.IsAPINamespaced(obj, c.Scheme, c.RESTMapper)
if err != nil {
return err
@@ -296,17 +296,47 @@ type multiNamespaceInformer struct {
var _ Informer = &multiNamespaceInformer{}
// AddEventHandler adds the handler to each namespaced informer.
func (i *multiNamespaceInformer) AddEventHandler(handler toolscache.ResourceEventHandler) {
for _, informer := range i.namespaceToInformer {
informer.AddEventHandler(handler)
func (i *multiNamespaceInformer) AddEventHandler(handler toolscache.ResourceEventHandler) (toolscache.ResourceEventHandlerRegistration, error) {
handles := make(map[string]toolscache.ResourceEventHandlerRegistration, len(i.namespaceToInformer))
for ns, informer := range i.namespaceToInformer {
registration, err := informer.AddEventHandler(handler)
if err != nil {
return nil, err
}
handles[ns] = registration
}
return handles, nil
}
// AddEventHandlerWithResyncPeriod adds the handler with a resync period to each namespaced informer.
func (i *multiNamespaceInformer) AddEventHandlerWithResyncPeriod(handler toolscache.ResourceEventHandler, resyncPeriod time.Duration) {
for _, informer := range i.namespaceToInformer {
informer.AddEventHandlerWithResyncPeriod(handler, resyncPeriod)
func (i *multiNamespaceInformer) AddEventHandlerWithResyncPeriod(handler toolscache.ResourceEventHandler, resyncPeriod time.Duration) (toolscache.ResourceEventHandlerRegistration, error) {
handles := make(map[string]toolscache.ResourceEventHandlerRegistration, len(i.namespaceToInformer))
for ns, informer := range i.namespaceToInformer {
registration, err := informer.AddEventHandlerWithResyncPeriod(handler, resyncPeriod)
if err != nil {
return nil, err
}
handles[ns] = registration
}
return handles, nil
}
// RemoveEventHandler removes a formerly added event handler given by its registration handle.
func (i *multiNamespaceInformer) RemoveEventHandler(h toolscache.ResourceEventHandlerRegistration) error {
handles, ok := h.(map[string]toolscache.ResourceEventHandlerRegistration)
if !ok {
return fmt.Errorf("it is not the registration returned by multiNamespaceInformer")
}
for ns, informer := range i.namespaceToInformer {
registration, ok := handles[ns]
if !ok {
continue
}
if err := informer.RemoveEventHandler(registration); err != nil {
return err
}
}
return nil
}
// AddIndexers adds the indexer for each namespaced informer.

View File

@@ -81,7 +81,7 @@ func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersi
// (unstructured, partial, etc)
// check for PartialObjectMetadata, which is analogous to unstructured, but isn't handled by ObjectKinds
_, isPartial := obj.(*metav1.PartialObjectMetadata) //nolint:ifshort
_, isPartial := obj.(*metav1.PartialObjectMetadata)
_, isPartialList := obj.(*metav1.PartialObjectMetadataList)
if isPartial || isPartialList {
// we require that the GVK be populated in order to recognize the object

View File

@@ -17,7 +17,6 @@ limitations under the License.
package apiutil
import (
"errors"
"sync"
"sync/atomic"
@@ -41,6 +40,8 @@ type dynamicRESTMapper struct {
// Used for lazy init.
inited uint32
initMtx sync.Mutex
useLazyRestmapper bool
}
// DynamicRESTMapperOption is a functional option on the dynamicRESTMapper.
@@ -61,6 +62,12 @@ var WithLazyDiscovery DynamicRESTMapperOption = func(drm *dynamicRESTMapper) err
return nil
}
// WithExperimentalLazyMapper enables experimental more advanced Lazy Restmapping mechanism.
var WithExperimentalLazyMapper DynamicRESTMapperOption = func(drm *dynamicRESTMapper) error {
drm.useLazyRestmapper = true
return nil
}
// WithCustomMapper supports setting a custom RESTMapper refresher instead of
// the default method, which uses a discovery client.
//
@@ -96,6 +103,9 @@ func NewDynamicRESTMapper(cfg *rest.Config, opts ...DynamicRESTMapperOption) (me
return nil, err
}
}
if drm.useLazyRestmapper {
return newLazyRESTMapperWithClient(client)
}
if !drm.lazy {
if err := drm.setStaticMapper(); err != nil {
return nil, err
@@ -145,7 +155,7 @@ func (drm *dynamicRESTMapper) init() (err error) {
// checkAndReload attempts to call the given callback, which is assumed to be dependent
// on the data in the restmapper.
//
// If the callback returns an error that matches the given error, it will attempt to reload
// If the callback returns an error matching meta.IsNoMatchErr, it will attempt to reload
// the RESTMapper's data and re-call the callback once that's occurred.
// If the callback returns any other error, the function will return immediately regardless.
//
@@ -154,7 +164,7 @@ func (drm *dynamicRESTMapper) init() (err error) {
// the callback.
// It's thread-safe, and worries about thread-safety for the callback (so the callback does
// not need to attempt to lock the restmapper).
func (drm *dynamicRESTMapper) checkAndReload(needsReloadErr error, checkNeedsReload func() error) error {
func (drm *dynamicRESTMapper) checkAndReload(checkNeedsReload func() error) error {
// first, check the common path -- data is fresh enough
// (use an IIFE for the lock's defer)
err := func() error {
@@ -164,10 +174,7 @@ func (drm *dynamicRESTMapper) checkAndReload(needsReloadErr error, checkNeedsRel
return checkNeedsReload()
}()
// NB(directxman12): `Is` and `As` have a confusing relationship --
// `Is` is like `== or does this implement .Is`, whereas `As` says
// `can I type-assert into`
needsReload := errors.As(err, &needsReloadErr)
needsReload := meta.IsNoMatchError(err)
if !needsReload {
return err
}
@@ -178,7 +185,7 @@ func (drm *dynamicRESTMapper) checkAndReload(needsReloadErr error, checkNeedsRel
// ... and double-check that we didn't reload in the meantime
err = checkNeedsReload()
needsReload = errors.As(err, &needsReloadErr)
needsReload = meta.IsNoMatchError(err)
if !needsReload {
return err
}
@@ -206,7 +213,7 @@ func (drm *dynamicRESTMapper) KindFor(resource schema.GroupVersionResource) (sch
return schema.GroupVersionKind{}, err
}
var gvk schema.GroupVersionKind
err := drm.checkAndReload(&meta.NoResourceMatchError{}, func() error {
err := drm.checkAndReload(func() error {
var err error
gvk, err = drm.staticMapper.KindFor(resource)
return err
@@ -219,7 +226,7 @@ func (drm *dynamicRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]
return nil, err
}
var gvks []schema.GroupVersionKind
err := drm.checkAndReload(&meta.NoResourceMatchError{}, func() error {
err := drm.checkAndReload(func() error {
var err error
gvks, err = drm.staticMapper.KindsFor(resource)
return err
@@ -233,7 +240,7 @@ func (drm *dynamicRESTMapper) ResourceFor(input schema.GroupVersionResource) (sc
}
var gvr schema.GroupVersionResource
err := drm.checkAndReload(&meta.NoResourceMatchError{}, func() error {
err := drm.checkAndReload(func() error {
var err error
gvr, err = drm.staticMapper.ResourceFor(input)
return err
@@ -246,7 +253,7 @@ func (drm *dynamicRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([
return nil, err
}
var gvrs []schema.GroupVersionResource
err := drm.checkAndReload(&meta.NoResourceMatchError{}, func() error {
err := drm.checkAndReload(func() error {
var err error
gvrs, err = drm.staticMapper.ResourcesFor(input)
return err
@@ -259,7 +266,7 @@ func (drm *dynamicRESTMapper) RESTMapping(gk schema.GroupKind, versions ...strin
return nil, err
}
var mapping *meta.RESTMapping
err := drm.checkAndReload(&meta.NoKindMatchError{}, func() error {
err := drm.checkAndReload(func() error {
var err error
mapping, err = drm.staticMapper.RESTMapping(gk, versions...)
return err
@@ -272,7 +279,7 @@ func (drm *dynamicRESTMapper) RESTMappings(gk schema.GroupKind, versions ...stri
return nil, err
}
var mappings []*meta.RESTMapping
err := drm.checkAndReload(&meta.NoKindMatchError{}, func() error {
err := drm.checkAndReload(func() error {
var err error
mappings, err = drm.staticMapper.RESTMappings(gk, versions...)
return err
@@ -285,7 +292,7 @@ func (drm *dynamicRESTMapper) ResourceSingularizer(resource string) (string, err
return "", err
}
var singular string
err := drm.checkAndReload(&meta.NoResourceMatchError{}, func() error {
err := drm.checkAndReload(func() error {
var err error
singular, err = drm.staticMapper.ResourceSingularizer(resource)
return err

View File

@@ -0,0 +1,248 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apiutil
import (
"fmt"
"sync"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/restmapper"
)
// lazyRESTMapper is a RESTMapper that will lazily query the provided
// client for discovery information to do REST mappings.
type lazyRESTMapper struct {
mapper meta.RESTMapper
client *discovery.DiscoveryClient
knownGroups map[string]*restmapper.APIGroupResources
apiGroups *metav1.APIGroupList
// mutex to provide thread-safe mapper reloading.
mu sync.Mutex
}
// newLazyRESTMapperWithClient initializes a LazyRESTMapper with a custom discovery client.
func newLazyRESTMapperWithClient(discoveryClient *discovery.DiscoveryClient) (meta.RESTMapper, error) {
return &lazyRESTMapper{
mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
client: discoveryClient,
knownGroups: map[string]*restmapper.APIGroupResources{},
}, nil
}
// KindFor implements Mapper.KindFor.
func (m *lazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
res, err := m.mapper.KindFor(resource)
if meta.IsNoMatchError(err) {
if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
return res, err
}
res, err = m.mapper.KindFor(resource)
}
return res, err
}
// KindsFor implements Mapper.KindsFor.
func (m *lazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
res, err := m.mapper.KindsFor(resource)
if meta.IsNoMatchError(err) {
if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
return res, err
}
res, err = m.mapper.KindsFor(resource)
}
return res, err
}
// ResourceFor implements Mapper.ResourceFor.
func (m *lazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
res, err := m.mapper.ResourceFor(input)
if meta.IsNoMatchError(err) {
if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
return res, err
}
res, err = m.mapper.ResourceFor(input)
}
return res, err
}
// ResourcesFor implements Mapper.ResourcesFor.
func (m *lazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
res, err := m.mapper.ResourcesFor(input)
if meta.IsNoMatchError(err) {
if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
return res, err
}
res, err = m.mapper.ResourcesFor(input)
}
return res, err
}
// RESTMapping implements Mapper.RESTMapping.
func (m *lazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
res, err := m.mapper.RESTMapping(gk, versions...)
if meta.IsNoMatchError(err) {
if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
return res, err
}
res, err = m.mapper.RESTMapping(gk, versions...)
}
return res, err
}
// RESTMappings implements Mapper.RESTMappings.
func (m *lazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
res, err := m.mapper.RESTMappings(gk, versions...)
if meta.IsNoMatchError(err) {
if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
return res, err
}
res, err = m.mapper.RESTMappings(gk, versions...)
}
return res, err
}
// ResourceSingularizer implements Mapper.ResourceSingularizer.
func (m *lazyRESTMapper) ResourceSingularizer(resource string) (string, error) {
return m.mapper.ResourceSingularizer(resource)
}
// addKnownGroupAndReload reloads the mapper with updated information about missing API group.
// versions can be specified for partial updates, for instance for v1beta1 version only.
func (m *lazyRESTMapper) addKnownGroupAndReload(groupName string, versions ...string) error {
m.mu.Lock()
defer m.mu.Unlock()
// If no specific versions are set by user, we will scan all available ones for the API group.
// This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls
// this data will be taken from cache.
if len(versions) == 0 {
apiGroup, err := m.findAPIGroupByName(groupName)
if err != nil {
return err
}
for _, version := range apiGroup.Versions {
versions = append(versions, version.Version)
}
}
// Create or fetch group resources from cache.
groupResources := &restmapper.APIGroupResources{
Group: metav1.APIGroup{Name: groupName},
VersionedResources: make(map[string][]metav1.APIResource),
}
if _, ok := m.knownGroups[groupName]; ok {
groupResources = m.knownGroups[groupName]
}
// Update information for group resources about versioned resources.
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
groupVersionResources, err := m.fetchGroupVersionResources(groupName, versions...)
if err != nil {
return fmt.Errorf("failed to get API group resources: %w", err)
}
for version, resources := range groupVersionResources {
groupResources.VersionedResources[version.Version] = resources.APIResources
}
// Update information for group resources about the API group by adding new versions.
for _, version := range versions {
groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{
GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(),
Version: version,
})
}
// Update data in the cache.
m.knownGroups[groupName] = groupResources
// Finally, update the group with received information and regenerate the mapper.
updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
for _, agr := range m.knownGroups {
updatedGroupResources = append(updatedGroupResources, agr)
}
m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
return nil
}
// findAPIGroupByName returns API group by its name.
func (m *lazyRESTMapper) findAPIGroupByName(groupName string) (metav1.APIGroup, error) {
// Ensure that required info about existing API groups is received and stored in the mapper.
// It will make 2 API calls to /api and /apis, but only once.
if m.apiGroups == nil {
apiGroups, err := m.client.ServerGroups()
if err != nil {
return metav1.APIGroup{}, fmt.Errorf("failed to get server groups: %w", err)
}
if len(apiGroups.Groups) == 0 {
return metav1.APIGroup{}, fmt.Errorf("received an empty API groups list")
}
m.apiGroups = apiGroups
}
for i := range m.apiGroups.Groups {
if groupName == (&m.apiGroups.Groups[i]).Name {
return m.apiGroups.Groups[i], nil
}
}
return metav1.APIGroup{}, fmt.Errorf("failed to find API group %s", groupName)
}
// fetchGroupVersionResources fetches the resources for the specified group and its versions.
func (m *lazyRESTMapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
failedGroups := make(map[schema.GroupVersion]error)
for _, version := range versions {
groupVersion := schema.GroupVersion{Group: groupName, Version: version}
apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
if err != nil {
failedGroups[groupVersion] = err
}
if apiResourceList != nil {
// even in case of error, some fallback might have been returned.
groupVersionResources[groupVersion] = apiResourceList
}
}
if len(failedGroups) > 0 {
return nil, &discovery.ErrGroupDiscoveryFailed{Groups: failedGroups}
}
return groupVersionResources, nil
}

View File

@@ -18,6 +18,7 @@ package client
import (
"context"
"errors"
"fmt"
"strings"
@@ -45,7 +46,7 @@ type WarningHandlerOptions struct {
// AllowDuplicateLogs does not deduplicate the to-be
// logged surfaced warnings messages. See
// log.WarningHandlerOptions for considerations
// regarding deuplication
// regarding deduplication
AllowDuplicateLogs bool
}
@@ -88,13 +89,12 @@ func newClient(config *rest.Config, options Options) (*client, error) {
// is log.KubeAPIWarningLogger with deduplication enabled.
// See log.KubeAPIWarningLoggerOptions for considerations
// regarding deduplication.
rest.SetDefaultWarningHandler(
log.NewKubeAPIWarningLogger(
logger,
log.KubeAPIWarningLoggerOptions{
Deduplicate: !options.Opts.AllowDuplicateLogs,
},
),
config = rest.CopyConfig(config)
config.WarningHandler = log.NewKubeAPIWarningLogger(
logger,
log.KubeAPIWarningLoggerOptions{
Deduplicate: !options.Opts.AllowDuplicateLogs,
},
)
}
@@ -241,16 +241,16 @@ func (c *client) Patch(ctx context.Context, obj Object, patch Patch, opts ...Pat
}
// Get implements client.Client.
func (c *client) Get(ctx context.Context, key ObjectKey, obj Object) error {
func (c *client) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
switch obj.(type) {
case *unstructured.Unstructured:
return c.unstructuredClient.Get(ctx, key, obj)
return c.unstructuredClient.Get(ctx, key, obj, opts...)
case *metav1.PartialObjectMetadata:
// Metadata only object should always preserve the GVK coming in from the caller.
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
return c.metadataClient.Get(ctx, key, obj)
return c.metadataClient.Get(ctx, key, obj, opts...)
default:
return c.typedClient.Get(ctx, key, obj)
return c.typedClient.Get(ctx, key, obj, opts...)
}
}
@@ -289,40 +289,194 @@ func (c *client) List(ctx context.Context, obj ObjectList, opts ...ListOption) e
}
// Status implements client.StatusClient.
func (c *client) Status() StatusWriter {
return &statusWriter{client: c}
func (c *client) Status() SubResourceWriter {
return c.SubResource("status")
}
// statusWriter is client.StatusWriter that writes status subresource.
type statusWriter struct {
client *client
func (c *client) SubResource(subResource string) SubResourceClient {
return &subResourceClient{client: c, subResource: subResource}
}
// ensure statusWriter implements client.StatusWriter.
var _ StatusWriter = &statusWriter{}
// subResourceClient is client.SubResourceWriter that writes to subresources.
type subResourceClient struct {
client *client
subResource string
}
// Update implements client.StatusWriter.
func (sw *statusWriter) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
defer sw.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
// ensure subResourceClient implements client.SubResourceClient.
var _ SubResourceClient = &subResourceClient{}
// SubResourceGetOptions holds all the possible configuration
// for a subresource Get request.
type SubResourceGetOptions struct {
Raw *metav1.GetOptions
}
// ApplyToSubResourceGet updates the configuaration to the given get options.
func (getOpt *SubResourceGetOptions) ApplyToSubResourceGet(o *SubResourceGetOptions) {
if getOpt.Raw != nil {
o.Raw = getOpt.Raw
}
}
// ApplyOptions applues the given options.
func (getOpt *SubResourceGetOptions) ApplyOptions(opts []SubResourceGetOption) *SubResourceGetOptions {
for _, o := range opts {
o.ApplyToSubResourceGet(getOpt)
}
return getOpt
}
// AsGetOptions returns the configured options as *metav1.GetOptions.
func (getOpt *SubResourceGetOptions) AsGetOptions() *metav1.GetOptions {
if getOpt.Raw == nil {
return &metav1.GetOptions{}
}
return getOpt.Raw
}
// SubResourceUpdateOptions holds all the possible configuration
// for a subresource update request.
type SubResourceUpdateOptions struct {
UpdateOptions
SubResourceBody Object
}
// ApplyToSubResourceUpdate updates the configuration on the given create options
func (uo *SubResourceUpdateOptions) ApplyToSubResourceUpdate(o *SubResourceUpdateOptions) {
uo.UpdateOptions.ApplyToUpdate(&o.UpdateOptions)
if uo.SubResourceBody != nil {
o.SubResourceBody = uo.SubResourceBody
}
}
// ApplyOptions applies the given options.
func (uo *SubResourceUpdateOptions) ApplyOptions(opts []SubResourceUpdateOption) *SubResourceUpdateOptions {
for _, o := range opts {
o.ApplyToSubResourceUpdate(uo)
}
return uo
}
// SubResourceUpdateAndPatchOption is an option that can be used for either
// a subresource update or patch request.
type SubResourceUpdateAndPatchOption interface {
SubResourceUpdateOption
SubResourcePatchOption
}
// WithSubResourceBody returns an option that uses the given body
// for a subresource Update or Patch operation.
func WithSubResourceBody(body Object) SubResourceUpdateAndPatchOption {
return &withSubresourceBody{body: body}
}
type withSubresourceBody struct {
body Object
}
func (wsr *withSubresourceBody) ApplyToSubResourceUpdate(o *SubResourceUpdateOptions) {
o.SubResourceBody = wsr.body
}
func (wsr *withSubresourceBody) ApplyToSubResourcePatch(o *SubResourcePatchOptions) {
o.SubResourceBody = wsr.body
}
// SubResourceCreateOptions are all the possible configurations for a subresource
// create request.
type SubResourceCreateOptions struct {
CreateOptions
}
// ApplyOptions applies the given options.
func (co *SubResourceCreateOptions) ApplyOptions(opts []SubResourceCreateOption) *SubResourceCreateOptions {
for _, o := range opts {
o.ApplyToSubResourceCreate(co)
}
return co
}
// ApplyToSubresourceCreate applies the the configuration on the given create options.
func (co *SubResourceCreateOptions) ApplyToSubresourceCreate(o *SubResourceCreateOptions) {
co.CreateOptions.ApplyToCreate(&co.CreateOptions)
}
// SubResourcePatchOptions holds all possible configurations for a subresource patch
// request.
type SubResourcePatchOptions struct {
PatchOptions
SubResourceBody Object
}
// ApplyOptions applies the given options.
func (po *SubResourcePatchOptions) ApplyOptions(opts []SubResourcePatchOption) *SubResourcePatchOptions {
for _, o := range opts {
o.ApplyToSubResourcePatch(po)
}
return po
}
// ApplyToSubResourcePatch applies the configuration on the given patch options.
func (po *SubResourcePatchOptions) ApplyToSubResourcePatch(o *SubResourcePatchOptions) {
po.PatchOptions.ApplyToPatch(&o.PatchOptions)
if po.SubResourceBody != nil {
o.SubResourceBody = po.SubResourceBody
}
}
func (sc *subResourceClient) Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error {
switch obj.(type) {
case *unstructured.Unstructured:
return sw.client.unstructuredClient.UpdateStatus(ctx, obj, opts...)
return sc.client.unstructuredClient.GetSubResource(ctx, obj, subResource, sc.subResource, opts...)
case *metav1.PartialObjectMetadata:
return errors.New("can not get subresource using only metadata")
default:
return sc.client.typedClient.GetSubResource(ctx, obj, subResource, sc.subResource, opts...)
}
}
// Create implements client.SubResourceClient
func (sc *subResourceClient) Create(ctx context.Context, obj Object, subResource Object, opts ...SubResourceCreateOption) error {
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
defer sc.client.resetGroupVersionKind(subResource, subResource.GetObjectKind().GroupVersionKind())
switch obj.(type) {
case *unstructured.Unstructured:
return sc.client.unstructuredClient.CreateSubResource(ctx, obj, subResource, sc.subResource, opts...)
case *metav1.PartialObjectMetadata:
return fmt.Errorf("cannot update status using only metadata -- did you mean to patch?")
default:
return sw.client.typedClient.UpdateStatus(ctx, obj, opts...)
return sc.client.typedClient.CreateSubResource(ctx, obj, subResource, sc.subResource, opts...)
}
}
// Patch implements client.Client.
func (sw *statusWriter) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
defer sw.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
// Update implements client.SubResourceClient
func (sc *subResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
switch obj.(type) {
case *unstructured.Unstructured:
return sw.client.unstructuredClient.PatchStatus(ctx, obj, patch, opts...)
return sc.client.unstructuredClient.UpdateSubResource(ctx, obj, sc.subResource, opts...)
case *metav1.PartialObjectMetadata:
return sw.client.metadataClient.PatchStatus(ctx, obj, patch, opts...)
return fmt.Errorf("cannot update status using only metadata -- did you mean to patch?")
default:
return sw.client.typedClient.PatchStatus(ctx, obj, patch, opts...)
return sc.client.typedClient.UpdateSubResource(ctx, obj, sc.subResource, opts...)
}
}
// Patch implements client.SubResourceWriter.
func (sc *subResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
switch obj.(type) {
case *unstructured.Unstructured:
return sc.client.unstructuredClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
case *metav1.PartialObjectMetadata:
return sc.client.metadataClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
default:
return sc.client.typedClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
}
}

View File

@@ -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.
@@ -47,7 +64,7 @@ func init() {
// It also applies saner defaults for QPS and burst based on the Kubernetes
// controller manager defaults (20 QPS, 30 burst)
//
// Config precedence
// Config precedence:
//
// * --kubeconfig flag pointing at a file
//
@@ -67,7 +84,7 @@ func GetConfig() (*rest.Config, error) {
// It also applies saner defaults for QPS and burst based on the Kubernetes
// controller manager defaults (20 QPS, 30 burst)
//
// Config precedence
// Config precedence:
//
// * --kubeconfig flag pointing at a file
//
@@ -96,7 +113,7 @@ func GetConfigWithContext(context string) (*rest.Config, error) {
var loadInClusterConfig = rest.InClusterConfig
// loadConfig loads a REST Config as per the rules specified in GetConfig.
func loadConfig(context string) (*rest.Config, error) {
func loadConfig(context string) (config *rest.Config, configErr error) {
// If a flag is specified with the config location, use that
if len(kubeconfig) > 0 {
return loadConfigWithContext("", &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, context)
@@ -106,9 +123,16 @@ func loadConfig(context string) (*rest.Config, error) {
// try the in-cluster config.
kubeconfigPath := os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
if len(kubeconfigPath) == 0 {
if c, err := loadInClusterConfig(); err == nil {
c, err := loadInClusterConfig()
if err == nil {
return c, nil
}
defer func() {
if configErr != nil {
log.Error(err, "unable to load in-cluster config")
}
}()
}
// If the recommended kubeconfig env variable is set, or there

View File

@@ -17,7 +17,7 @@ limitations under the License.
// Package client contains functionality for interacting with Kubernetes API
// servers.
//
// Clients
// # Clients
//
// Clients are split into two interfaces -- Readers and Writers. Readers
// get and list, while writers create, update, and delete.
@@ -25,18 +25,19 @@ limitations under the License.
// The New function can be used to create a new client that talks directly
// to the API server.
//
// A common pattern in Kubernetes to read from a cache and write to the API
// It is a common pattern in Kubernetes to read from a cache and write to the API
// server. This pattern is covered by the DelegatingClient type, which can
// be used to have a client whose Reader is different from the Writer.
//
// Options
// # Options
//
// Many client operations in Kubernetes support options. These options are
// represented as variadic arguments at the end of a given method call.
// For instance, to use a label selector on list, you can call
// err := someReader.List(context.Background(), &podList, client.MatchingLabels{"somelabel": "someval"})
//
// Indexing
// err := someReader.List(context.Background(), &podList, client.MatchingLabels{"somelabel": "someval"})
//
// # Indexing
//
// Indexes may be added to caches using a FieldIndexer. This allows you to easily
// and efficiently look up objects with certain properties. You can then make

View File

@@ -72,8 +72,8 @@ func (c *dryRunClient) Patch(ctx context.Context, obj Object, patch Patch, opts
}
// Get implements client.Client.
func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj Object) error {
return c.client.Get(ctx, key, obj)
func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
return c.client.Get(ctx, key, obj, opts...)
}
// List implements client.Client.
@@ -82,25 +82,38 @@ func (c *dryRunClient) List(ctx context.Context, obj ObjectList, opts ...ListOpt
}
// Status implements client.StatusClient.
func (c *dryRunClient) Status() StatusWriter {
return &dryRunStatusWriter{client: c.client.Status()}
func (c *dryRunClient) Status() SubResourceWriter {
return c.SubResource("status")
}
// ensure dryRunStatusWriter implements client.StatusWriter.
var _ StatusWriter = &dryRunStatusWriter{}
// SubResource implements client.SubResourceClient.
func (c *dryRunClient) SubResource(subResource string) SubResourceClient {
return &dryRunSubResourceClient{client: c.client.SubResource(subResource)}
}
// dryRunStatusWriter is client.StatusWriter that writes status subresource with dryRun mode
// ensure dryRunSubResourceWriter implements client.SubResourceWriter.
var _ SubResourceWriter = &dryRunSubResourceClient{}
// dryRunSubResourceClient is client.SubResourceWriter that writes status subresource with dryRun mode
// enforced.
type dryRunStatusWriter struct {
client StatusWriter
type dryRunSubResourceClient struct {
client SubResourceClient
}
// Update implements client.StatusWriter.
func (sw *dryRunStatusWriter) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
func (sw *dryRunSubResourceClient) Get(ctx context.Context, obj, subResource Object, opts ...SubResourceGetOption) error {
return sw.client.Get(ctx, obj, subResource, opts...)
}
func (sw *dryRunSubResourceClient) Create(ctx context.Context, obj, subResource Object, opts ...SubResourceCreateOption) error {
return sw.client.Create(ctx, obj, subResource, append(opts, DryRunAll)...)
}
// Update implements client.SubResourceWriter.
func (sw *dryRunSubResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
return sw.client.Update(ctx, obj, append(opts, DryRunAll)...)
}
// Patch implements client.StatusWriter.
func (sw *dryRunStatusWriter) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
// Patch implements client.SubResourceWriter.
func (sw *dryRunSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
return sw.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...)
}

View File

@@ -30,6 +30,8 @@ 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"
@@ -37,6 +39,7 @@ import (
"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"
@@ -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
}
@@ -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.
@@ -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 {
@@ -171,6 +221,7 @@ func (f *ClientBuilder) Build() client.WithWatch {
tracker: tracker,
scheme: f.scheme,
restMapper: f.restMapper,
indexes: f.indexes,
}
}
@@ -329,7 +380,7 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
return t.ObjectTracker.Update(gvr, obj, ns)
}
func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error {
func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
gvr, err := getGVRFromObject(obj, c.scheme)
if err != nil {
return err
@@ -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 {
@@ -634,8 +752,12 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
return err
}
func (c *fakeClient) Status() client.StatusWriter {
return &fakeStatusWriter{client: c}
func (c *fakeClient) Status() client.SubResourceWriter {
return c.SubResource("status")
}
func (c *fakeClient) SubResource(subResource string) client.SubResourceClient {
return &fakeSubResourceClient{client: c}
}
func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
@@ -664,20 +786,44 @@ func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupV
return gvr, nil
}
type fakeStatusWriter struct {
type fakeSubResourceClient struct {
client *fakeClient
}
func (sw *fakeStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
// TODO(droot): This results in full update of the obj (spec + status). Need
// a way to update status field only.
return sw.client.Update(ctx, obj, opts...)
func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error {
panic("fakeSubResourceClient does not support get")
}
func (sw *fakeStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
// TODO(droot): This results in full update of the obj (spec + status). Need
// a way to update status field only.
return sw.client.Patch(ctx, obj, patch, opts...)
func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
panic("fakeSubResourceWriter does not support create")
}
func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
// TODO(droot): This results in full update of the obj (spec + subresources). Need
// a way to update subresource only.
updateOptions := client.SubResourceUpdateOptions{}
updateOptions.ApplyOptions(opts)
body := obj
if updateOptions.SubResourceBody != nil {
body = updateOptions.SubResourceBody
}
return sw.client.Update(ctx, body, &updateOptions.UpdateOptions)
}
func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
// TODO(droot): This results in full update of the obj (spec + subresources). Need
// a way to update subresource only.
patchOptions := client.SubResourcePatchOptions{}
patchOptions.ApplyOptions(opts)
body := obj
if patchOptions.SubResourceBody != nil {
body = patchOptions.SubResourceBody
}
return sw.client.Patch(ctx, body, patch, &patchOptions.PatchOptions)
}
func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool {

View File

@@ -28,12 +28,11 @@ When in doubt, it's almost always better not to use this package and instead use
envtest.Environment with a real client and API server.
WARNING: ⚠️ Current Limitations / Known Issues with the fake Client ⚠️
- This client does not have a way to inject specific errors to test handled vs. unhandled errors.
- There is some support for sub resources which can cause issues with tests if you're trying to update
e.g. metadata and status in the same reconcile.
- No OpeanAPI validation is performed when creating or updating objects.
- ObjectMeta's `Generation` and `ResourceVersion` don't behave properly, Patch or Update
operations that rely on these fields will fail, or give false positives.
- This client does not have a way to inject specific errors to test handled vs. unhandled errors.
- There is some support for sub resources which can cause issues with tests if you're trying to update
e.g. metadata and status in the same reconcile.
- No OpenAPI validation is performed when creating or updating objects.
- ObjectMeta's `Generation` and `ResourceVersion` don't behave properly, Patch or Update
operations that rely on these fields will fail, or give false positives.
*/
package fake

View File

@@ -50,7 +50,7 @@ type Reader interface {
// Get retrieves an obj for the given object key from the Kubernetes Cluster.
// obj must be a struct pointer so that obj can be updated with the response
// returned by the Server.
Get(ctx context.Context, key ObjectKey, obj Object) error
Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error
// List retrieves list of objects for a given namespace and list options. On a
// successful call, Items field in the list will be populated with the
@@ -60,7 +60,8 @@ type Reader interface {
// Writer knows how to create, delete, and update Kubernetes objects.
type Writer interface {
// Create saves the object obj in the Kubernetes cluster.
// Create saves the object obj in the Kubernetes cluster. obj must be a
// struct pointer so that obj can be updated with the content returned by the Server.
Create(ctx context.Context, obj Object, opts ...CreateOption) error
// Delete deletes the given obj from Kubernetes cluster.
@@ -81,20 +82,80 @@ type Writer interface {
// StatusClient knows how to create a client which can update status subresource
// for kubernetes objects.
type StatusClient interface {
Status() StatusWriter
Status() SubResourceWriter
}
// StatusWriter knows how to update status subresource of a Kubernetes object.
type StatusWriter interface {
// SubResourceClientConstructor knows how to create a client which can update subresource
// for kubernetes objects.
type SubResourceClientConstructor interface {
// SubResourceClientConstructor returns a subresource client for the named subResource. Known
// upstream subResources usages are:
// - ServiceAccount token creation:
// sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// token := &authenticationv1.TokenRequest{}
// c.SubResourceClient("token").Create(ctx, sa, token)
//
// - Pod eviction creation:
// pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// c.SubResourceClient("eviction").Create(ctx, pod, &policyv1.Eviction{})
//
// - Pod binding creation:
// pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// binding := &corev1.Binding{Target: corev1.ObjectReference{Name: "my-node"}}
// c.SubResourceClient("binding").Create(ctx, pod, binding)
//
// - CertificateSigningRequest approval:
// csr := &certificatesv1.CertificateSigningRequest{
// ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"},
// Status: certificatesv1.CertificateSigningRequestStatus{
// Conditions: []certificatesv1.[]CertificateSigningRequestCondition{{
// Type: certificatesv1.CertificateApproved,
// Status: corev1.ConditionTrue,
// }},
// },
// }
// c.SubResourceClient("approval").Update(ctx, csr)
//
// - Scale retrieval:
// dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// scale := &autoscalingv1.Scale{}
// c.SubResourceClient("scale").Get(ctx, dep, scale)
//
// - Scale update:
// dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}
// scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}}
// c.SubResourceClient("scale").Update(ctx, dep, client.WithSubResourceBody(scale))
SubResource(subResource string) SubResourceClient
}
// StatusWriter is kept for backward compatibility.
type StatusWriter = SubResourceWriter
// SubResourceReader knows how to read SubResources
type SubResourceReader interface {
Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error
}
// SubResourceWriter knows how to update subresource of a Kubernetes object.
type SubResourceWriter interface {
// Create saves the subResource object in the Kubernetes cluster. obj must be a
// struct pointer so that obj can be updated with the content returned by the Server.
Create(ctx context.Context, obj Object, subResource Object, opts ...SubResourceCreateOption) error
// Update updates the fields corresponding to the status subresource for the
// given obj. obj must be a struct pointer so that obj can be updated
// with the content returned by the Server.
Update(ctx context.Context, obj Object, opts ...UpdateOption) error
Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error
// Patch patches the given object's subresource. obj must be a struct
// pointer so that obj can be updated with the content returned by the
// Server.
Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error
Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error
}
// SubResourceClient knows how to perform CRU operations on Kubernetes objects.
type SubResourceClient interface {
SubResourceReader
SubResourceWriter
}
// Client knows how to perform CRUD operations on Kubernetes objects.
@@ -102,6 +163,7 @@ type Client interface {
Reader
Writer
StatusClient
SubResourceClientConstructor
// Scheme returns the scheme this client is using.
Scheme() *runtime.Scheme
@@ -143,3 +205,13 @@ func IgnoreNotFound(err error) error {
}
return err
}
// IgnoreAlreadyExists returns nil on AlreadyExists errors.
// All other values that are not AlreadyExists errors or nil are returned unmodified.
func IgnoreAlreadyExists(err error) error {
if apierrors.IsAlreadyExists(err) {
return nil
}
return err
}

View File

@@ -116,7 +116,7 @@ func (mc *metadataClient) Patch(ctx context.Context, obj Object, patch Patch, op
}
// Get implements client.Client.
func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object) error {
func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
metadata, ok := obj.(*metav1.PartialObjectMetadata)
if !ok {
return fmt.Errorf("metadata client did not understand object: %T", obj)
@@ -124,12 +124,15 @@ func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object) er
gvk := metadata.GroupVersionKind()
getOpts := GetOptions{}
getOpts.ApplyOptions(opts)
resInt, err := mc.getResourceInterface(gvk, key.Namespace)
if err != nil {
return err
}
res, err := resInt.Get(ctx, key.Name, metav1.GetOptions{})
res, err := resInt.Get(ctx, key.Name, *getOpts.AsGetOptions())
if err != nil {
return err
}
@@ -165,7 +168,7 @@ func (mc *metadataClient) List(ctx context.Context, obj ObjectList, opts ...List
return nil
}
func (mc *metadataClient) PatchStatus(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
func (mc *metadataClient) PatchSubResource(ctx context.Context, obj Object, subResource string, patch Patch, opts ...SubResourcePatchOption) error {
metadata, ok := obj.(*metav1.PartialObjectMetadata)
if !ok {
return fmt.Errorf("metadata client did not understand object: %T", obj)
@@ -177,16 +180,24 @@ func (mc *metadataClient) PatchStatus(ctx context.Context, obj Object, patch Pat
return err
}
data, err := patch.Data(obj)
patchOpts := &SubResourcePatchOptions{}
patchOpts.ApplyOptions(opts)
body := obj
if patchOpts.SubResourceBody != nil {
body = patchOpts.SubResourceBody
}
data, err := patch.Data(body)
if err != nil {
return err
}
patchOpts := &PatchOptions{}
res, err := resInt.Patch(ctx, metadata.Name, patch.Type(), data, *patchOpts.AsPatchOptions(), "status")
res, err := resInt.Patch(ctx, metadata.Name, patch.Type(), data, *patchOpts.AsPatchOptions(), subResource)
if err != nil {
return err
}
*metadata = *res
metadata.SetGroupVersionKind(gvk) // restore the GVK, which isn't set on metadata
return nil

View File

@@ -52,7 +52,7 @@ func (n *namespacedClient) RESTMapper() meta.RESTMapper {
return n.client.RESTMapper()
}
// Create implements clinet.Client.
// Create implements client.Client.
func (n *namespacedClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, n.Scheme(), n.RESTMapper())
if err != nil {
@@ -138,18 +138,18 @@ func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, o
}
// Get implements client.Client.
func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object) error {
func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, n.Scheme(), n.RESTMapper())
if err != nil {
return fmt.Errorf("error finding the scope of the object: %w", err)
}
if isNamespaceScoped {
if key.Namespace != "" && key.Namespace != n.namespace {
return fmt.Errorf("namespace %s provided for the object %s does not match the namesapce %s on the client", key.Namespace, obj.GetName(), n.namespace)
return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client", key.Namespace, obj.GetName(), n.namespace)
}
key.Namespace = n.namespace
}
return n.client.Get(ctx, key, obj)
return n.client.Get(ctx, key, obj, opts...)
}
// List implements client.Client.
@@ -161,21 +161,80 @@ func (n *namespacedClient) List(ctx context.Context, obj ObjectList, opts ...Lis
}
// Status implements client.StatusClient.
func (n *namespacedClient) Status() StatusWriter {
return &namespacedClientStatusWriter{StatusClient: n.client.Status(), namespace: n.namespace, namespacedclient: n}
func (n *namespacedClient) Status() SubResourceWriter {
return n.SubResource("status")
}
// ensure namespacedClientStatusWriter implements client.StatusWriter.
var _ StatusWriter = &namespacedClientStatusWriter{}
// SubResource implements client.SubResourceClient.
func (n *namespacedClient) SubResource(subResource string) SubResourceClient {
return &namespacedClientSubResourceClient{client: n.client.SubResource(subResource), namespace: n.namespace, namespacedclient: n}
}
type namespacedClientStatusWriter struct {
StatusClient StatusWriter
// ensure namespacedClientSubResourceClient implements client.SubResourceClient.
var _ SubResourceClient = &namespacedClientSubResourceClient{}
type namespacedClientSubResourceClient struct {
client SubResourceClient
namespace string
namespacedclient Client
}
// Update implements client.StatusWriter.
func (nsw *namespacedClientStatusWriter) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
func (nsw *namespacedClientSubResourceClient) Get(ctx context.Context, obj, subResource Object, opts ...SubResourceGetOption) error {
isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, nsw.namespacedclient.Scheme(), nsw.namespacedclient.RESTMapper())
if err != nil {
return fmt.Errorf("error finding the scope of the object: %w", err)
}
objectNamespace := obj.GetNamespace()
if objectNamespace != nsw.namespace && objectNamespace != "" {
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace)
}
if isNamespaceScoped && objectNamespace == "" {
obj.SetNamespace(nsw.namespace)
}
return nsw.client.Get(ctx, obj, subResource, opts...)
}
func (nsw *namespacedClientSubResourceClient) Create(ctx context.Context, obj, subResource Object, opts ...SubResourceCreateOption) error {
isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, nsw.namespacedclient.Scheme(), nsw.namespacedclient.RESTMapper())
if err != nil {
return fmt.Errorf("error finding the scope of the object: %w", err)
}
objectNamespace := obj.GetNamespace()
if objectNamespace != nsw.namespace && objectNamespace != "" {
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace)
}
if isNamespaceScoped && objectNamespace == "" {
obj.SetNamespace(nsw.namespace)
}
return nsw.client.Create(ctx, obj, subResource, opts...)
}
// Update implements client.SubResourceWriter.
func (nsw *namespacedClientSubResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, nsw.namespacedclient.Scheme(), nsw.namespacedclient.RESTMapper())
if err != nil {
return fmt.Errorf("error finding the scope of the object: %w", err)
}
objectNamespace := obj.GetNamespace()
if objectNamespace != nsw.namespace && objectNamespace != "" {
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace)
}
if isNamespaceScoped && objectNamespace == "" {
obj.SetNamespace(nsw.namespace)
}
return nsw.client.Update(ctx, obj, opts...)
}
// Patch implements client.SubResourceWriter.
func (nsw *namespacedClientSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, nsw.namespacedclient.Scheme(), nsw.namespacedclient.RESTMapper())
if err != nil {
@@ -190,24 +249,5 @@ func (nsw *namespacedClientStatusWriter) Update(ctx context.Context, obj Object,
if isNamespaceScoped && objectNamespace == "" {
obj.SetNamespace(nsw.namespace)
}
return nsw.StatusClient.Update(ctx, obj, opts...)
}
// Patch implements client.StatusWriter.
func (nsw *namespacedClientStatusWriter) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, nsw.namespacedclient.Scheme(), nsw.namespacedclient.RESTMapper())
if err != nil {
return fmt.Errorf("error finding the scope of the object: %w", err)
}
objectNamespace := obj.GetNamespace()
if objectNamespace != nsw.namespace && objectNamespace != "" {
return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace)
}
if isNamespaceScoped && objectNamespace == "" {
obj.SetNamespace(nsw.namespace)
}
return nsw.StatusClient.Patch(ctx, obj, patch, opts...)
return nsw.client.Patch(ctx, obj, patch, opts...)
}

View File

@@ -37,6 +37,12 @@ type DeleteOption interface {
ApplyToDelete(*DeleteOptions)
}
// GetOption is some configuration that modifies options for a get request.
type GetOption interface {
// ApplyToGet applies this configuration to the given get options.
ApplyToGet(*GetOptions)
}
// ListOption is some configuration that modifies options for a list request.
type ListOption interface {
// ApplyToList applies this configuration to the given list options.
@@ -61,6 +67,29 @@ type DeleteAllOfOption interface {
ApplyToDeleteAllOf(*DeleteAllOfOptions)
}
// SubResourceGetOption modifies options for a SubResource Get request.
type SubResourceGetOption interface {
ApplyToSubResourceGet(*SubResourceGetOptions)
}
// SubResourceUpdateOption is some configuration that modifies options for a update request.
type SubResourceUpdateOption interface {
// ApplyToSubResourceUpdate applies this configuration to the given update options.
ApplyToSubResourceUpdate(*SubResourceUpdateOptions)
}
// SubResourceCreateOption is some configuration that modifies options for a create request.
type SubResourceCreateOption interface {
// ApplyToSubResourceCreate applies this configuration to the given create options.
ApplyToSubResourceCreate(*SubResourceCreateOptions)
}
// SubResourcePatchOption configures a subresource patch request.
type SubResourcePatchOption interface {
// ApplyToSubResourcePatch applies the configuration on the given patch options.
ApplyToSubResourcePatch(*SubResourcePatchOptions)
}
// }}}
// {{{ Multi-Type Options
@@ -90,10 +119,23 @@ func (dryRunAll) ApplyToPatch(opts *PatchOptions) {
func (dryRunAll) ApplyToDelete(opts *DeleteOptions) {
opts.DryRun = []string{metav1.DryRunAll}
}
func (dryRunAll) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) {
opts.DryRun = []string{metav1.DryRunAll}
}
func (dryRunAll) ApplyToSubResourceCreate(opts *SubResourceCreateOptions) {
opts.DryRun = []string{metav1.DryRunAll}
}
func (dryRunAll) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) {
opts.DryRun = []string{metav1.DryRunAll}
}
func (dryRunAll) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) {
opts.DryRun = []string{metav1.DryRunAll}
}
// FieldOwner set the field manager name for the given server-side apply patch.
type FieldOwner string
@@ -112,6 +154,21 @@ func (f FieldOwner) ApplyToUpdate(opts *UpdateOptions) {
opts.FieldManager = string(f)
}
// ApplyToSubResourcePatch applies this configuration to the given patch options.
func (f FieldOwner) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) {
opts.FieldManager = string(f)
}
// ApplyToSubResourceCreate applies this configuration to the given create options.
func (f FieldOwner) ApplyToSubResourceCreate(opts *SubResourceCreateOptions) {
opts.FieldManager = string(f)
}
// ApplyToSubResourceUpdate applies this configuration to the given update options.
func (f FieldOwner) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) {
opts.FieldManager = string(f)
}
// }}}
// {{{ Create Options
@@ -311,6 +368,45 @@ func (p PropagationPolicy) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) {
// }}}
// {{{ Get Options
// GetOptions contains options for get operation.
// Now it only has a Raw field, with support for specific resourceVersion.
type GetOptions struct {
// Raw represents raw GetOptions, as passed to the API server. Note
// that these may not be respected by all implementations of interface.
Raw *metav1.GetOptions
}
var _ GetOption = &GetOptions{}
// ApplyToGet implements GetOption for GetOptions.
func (o *GetOptions) ApplyToGet(lo *GetOptions) {
if o.Raw != nil {
lo.Raw = o.Raw
}
}
// AsGetOptions returns these options as a flattened metav1.GetOptions.
// This may mutate the Raw field.
func (o *GetOptions) AsGetOptions() *metav1.GetOptions {
if o == nil || o.Raw == nil {
return &metav1.GetOptions{}
}
return o.Raw
}
// ApplyOptions applies the given get options on these options,
// and then returns itself (for convenient chaining).
func (o *GetOptions) ApplyOptions(opts []GetOption) *GetOptions {
for _, opt := range opts {
opt.ApplyToGet(o)
}
return o
}
// }}}
// {{{ List Options
// ListOptions contains options for limiting or filtering results.
@@ -341,6 +437,12 @@ type ListOptions struct {
// it has expired. This field is not supported if watch is true in the Raw ListOptions.
Continue string
// UnsafeDisableDeepCopy indicates not to deep copy objects during list objects.
// Be very careful with this, when enabled you must DeepCopy any object before mutating it,
// otherwise you will mutate the object in the cache.
// +optional
UnsafeDisableDeepCopy *bool
// Raw represents raw ListOptions, as passed to the API server. Note
// that these may not be respected by all implementations of interface,
// and the LabelSelector, FieldSelector, Limit and Continue fields are ignored.
@@ -369,6 +471,9 @@ func (o *ListOptions) ApplyToList(lo *ListOptions) {
if o.Continue != "" {
lo.Continue = o.Continue
}
if o.UnsafeDisableDeepCopy != nil {
lo.UnsafeDisableDeepCopy = o.UnsafeDisableDeepCopy
}
}
// AsListOptions returns these options as a flattened metav1.ListOptions.
@@ -511,6 +616,25 @@ func (l Limit) ApplyToList(opts *ListOptions) {
opts.Limit = int64(l)
}
// UnsafeDisableDeepCopyOption indicates not to deep copy objects during list objects.
// Be very careful with this, when enabled you must DeepCopy any object before mutating it,
// otherwise you will mutate the object in the cache.
type UnsafeDisableDeepCopyOption bool
// ApplyToList applies this configuration to the given an List options.
func (d UnsafeDisableDeepCopyOption) ApplyToList(opts *ListOptions) {
definitelyTrue := true
definitelyFalse := false
if d {
opts.UnsafeDisableDeepCopy = &definitelyTrue
} else {
opts.UnsafeDisableDeepCopy = &definitelyFalse
}
}
// UnsafeDisableDeepCopy indicates not to deep copy objects during list objects.
const UnsafeDisableDeepCopy = UnsafeDisableDeepCopyOption(true)
// Continue sets a continuation token to retrieve chunks of results when using limit.
// Continue does not implement DeleteAllOfOption interface because the server
// does not support setting it for deletecollection operations.

View File

@@ -19,7 +19,7 @@ package client
import (
"fmt"
jsonpatch "github.com/evanphx/json-patch"
jsonpatch "github.com/evanphx/json-patch/v5"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/strategicpatch"

View File

@@ -61,8 +61,9 @@ func NewDelegatingClient(in NewDelegatingClientInput) (Client, error) {
uncachedGVKs: uncachedGVKs,
cacheUnstructured: in.CacheUnstructured,
},
Writer: in.Client,
StatusClient: in.Client,
Writer: in.Client,
StatusClient: in.Client,
SubResourceClientConstructor: in.Client,
}, nil
}
@@ -70,6 +71,7 @@ type delegatingClient struct {
Reader
Writer
StatusClient
SubResourceClientConstructor
scheme *runtime.Scheme
mapper meta.RESTMapper
@@ -121,13 +123,13 @@ func (d *delegatingReader) shouldBypassCache(obj runtime.Object) (bool, error) {
}
// Get retrieves an obj for a given object key from the Kubernetes Cluster.
func (d *delegatingReader) Get(ctx context.Context, key ObjectKey, obj Object) error {
func (d *delegatingReader) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
if isUncached, err := d.shouldBypassCache(obj); err != nil {
return err
} else if isUncached {
return d.ClientReader.Get(ctx, key, obj)
return d.ClientReader.Get(ctx, key, obj, opts...)
}
return d.CacheReader.Get(ctx, key, obj)
return d.CacheReader.Get(ctx, key, obj, opts...)
}
// List retrieves list of objects for a given namespace and list options.

View File

@@ -24,7 +24,6 @@ import (
var _ Reader = &typedClient{}
var _ Writer = &typedClient{}
var _ StatusWriter = &typedClient{}
// client is a client.Client that reads and writes directly from/to an API server. It lazily initializes
// new clients at the time they are used, and caches the client.
@@ -42,6 +41,7 @@ func (c *typedClient) Create(ctx context.Context, obj Object, opts ...CreateOpti
createOpts := &CreateOptions{}
createOpts.ApplyOptions(opts)
return o.Post().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
@@ -60,6 +60,7 @@ func (c *typedClient) Update(ctx context.Context, obj Object, opts ...UpdateOpti
updateOpts := &UpdateOptions{}
updateOpts.ApplyOptions(opts)
return o.Put().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
@@ -121,25 +122,30 @@ func (c *typedClient) Patch(ctx context.Context, obj Object, patch Patch, opts .
}
patchOpts := &PatchOptions{}
patchOpts.ApplyOptions(opts)
return o.Patch(patch.Type()).
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
VersionedParams(patchOpts.ApplyOptions(opts).AsPatchOptions(), c.paramCodec).
VersionedParams(patchOpts.AsPatchOptions(), c.paramCodec).
Body(data).
Do(ctx).
Into(obj)
}
// Get implements client.Client.
func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object) error {
func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
r, err := c.cache.getResource(obj)
if err != nil {
return err
}
getOpts := GetOptions{}
getOpts.ApplyOptions(opts)
return r.Get().
NamespaceIfScoped(key.Namespace, r.isNamespaced()).
Resource(r.resource()).
VersionedParams(getOpts.AsGetOptions(), c.paramCodec).
Name(key.Name).Do(ctx).Into(obj)
}
@@ -149,8 +155,10 @@ func (c *typedClient) List(ctx context.Context, obj ObjectList, opts ...ListOpti
if err != nil {
return err
}
listOpts := ListOptions{}
listOpts.ApplyOptions(opts)
return r.Get().
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
Resource(r.resource()).
@@ -159,8 +167,55 @@ func (c *typedClient) List(ctx context.Context, obj ObjectList, opts ...ListOpti
Into(obj)
}
// UpdateStatus used by StatusWriter to write status.
func (c *typedClient) UpdateStatus(ctx context.Context, obj Object, opts ...UpdateOption) error {
func (c *typedClient) GetSubResource(ctx context.Context, obj, subResourceObj Object, subResource string, opts ...SubResourceGetOption) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
}
if subResourceObj.GetName() == "" {
subResourceObj.SetName(obj.GetName())
}
getOpts := &SubResourceGetOptions{}
getOpts.ApplyOptions(opts)
return o.Get().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource(subResource).
VersionedParams(getOpts.AsGetOptions(), c.paramCodec).
Do(ctx).
Into(subResourceObj)
}
func (c *typedClient) CreateSubResource(ctx context.Context, obj Object, subResourceObj Object, subResource string, opts ...SubResourceCreateOption) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
}
if subResourceObj.GetName() == "" {
subResourceObj.SetName(obj.GetName())
}
createOpts := &SubResourceCreateOptions{}
createOpts.ApplyOptions(opts)
return o.Post().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource(subResource).
Body(subResourceObj).
VersionedParams(createOpts.AsCreateOptions(), c.paramCodec).
Do(ctx).
Into(subResourceObj)
}
// UpdateSubResource used by SubResourceWriter to write status.
func (c *typedClient) UpdateSubResource(ctx context.Context, obj Object, subResource string, opts ...SubResourceUpdateOption) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
@@ -169,37 +224,58 @@ func (c *typedClient) UpdateStatus(ctx context.Context, obj Object, opts ...Upda
// wrapped to improve the UX ?
// It will be nice to receive an error saying the object doesn't implement
// status subresource and check CRD definition
updateOpts := &SubResourceUpdateOptions{}
updateOpts.ApplyOptions(opts)
body := obj
if updateOpts.SubResourceBody != nil {
body = updateOpts.SubResourceBody
}
if body.GetName() == "" {
body.SetName(obj.GetName())
}
if body.GetNamespace() == "" {
body.SetNamespace(obj.GetNamespace())
}
return o.Put().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource("status").
Body(obj).
VersionedParams((&UpdateOptions{}).ApplyOptions(opts).AsUpdateOptions(), c.paramCodec).
SubResource(subResource).
Body(body).
VersionedParams(updateOpts.AsUpdateOptions(), c.paramCodec).
Do(ctx).
Into(obj)
Into(body)
}
// PatchStatus used by StatusWriter to write status.
func (c *typedClient) PatchStatus(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
// PatchSubResource used by SubResourceWriter to write subresource.
func (c *typedClient) PatchSubResource(ctx context.Context, obj Object, subResource string, patch Patch, opts ...SubResourcePatchOption) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
}
data, err := patch.Data(obj)
patchOpts := &SubResourcePatchOptions{}
patchOpts.ApplyOptions(opts)
body := obj
if patchOpts.SubResourceBody != nil {
body = patchOpts.SubResourceBody
}
data, err := patch.Data(body)
if err != nil {
return err
}
patchOpts := &PatchOptions{}
return o.Patch(patch.Type()).
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource("status").
SubResource(subResource).
Body(data).
VersionedParams(patchOpts.ApplyOptions(opts).AsPatchOptions(), c.paramCodec).
VersionedParams(patchOpts.AsPatchOptions(), c.paramCodec).
Do(ctx).
Into(obj)
Into(body)
}

View File

@@ -27,7 +27,6 @@ import (
var _ Reader = &unstructuredClient{}
var _ Writer = &unstructuredClient{}
var _ StatusWriter = &unstructuredClient{}
// client is a client.Client that reads and writes directly from/to an API server. It lazily initializes
// new clients at the time they are used, and caches the client.
@@ -52,6 +51,7 @@ func (uc *unstructuredClient) Create(ctx context.Context, obj Object, opts ...Cr
createOpts := &CreateOptions{}
createOpts.ApplyOptions(opts)
result := o.Post().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
@@ -80,6 +80,7 @@ func (uc *unstructuredClient) Update(ctx context.Context, obj Object, opts ...Up
updateOpts := UpdateOptions{}
updateOpts.ApplyOptions(opts)
result := o.Put().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
@@ -106,6 +107,7 @@ func (uc *unstructuredClient) Delete(ctx context.Context, obj Object, opts ...De
deleteOpts := DeleteOptions{}
deleteOpts.ApplyOptions(opts)
return o.Delete().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
@@ -128,6 +130,7 @@ func (uc *unstructuredClient) DeleteAllOf(ctx context.Context, obj Object, opts
deleteAllOfOpts := DeleteAllOfOptions{}
deleteAllOfOpts.ApplyOptions(opts)
return o.Delete().
NamespaceIfScoped(deleteAllOfOpts.ListOptions.Namespace, o.isNamespaced()).
Resource(o.resource()).
@@ -154,18 +157,20 @@ func (uc *unstructuredClient) Patch(ctx context.Context, obj Object, patch Patch
}
patchOpts := &PatchOptions{}
patchOpts.ApplyOptions(opts)
return o.Patch(patch.Type()).
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
VersionedParams(patchOpts.ApplyOptions(opts).AsPatchOptions(), uc.paramCodec).
VersionedParams(patchOpts.AsPatchOptions(), uc.paramCodec).
Body(data).
Do(ctx).
Into(obj)
}
// Get implements client.Client.
func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object) error {
func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
@@ -173,6 +178,9 @@ func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object
gvk := u.GroupVersionKind()
getOpts := GetOptions{}
getOpts.ApplyOptions(opts)
r, err := uc.cache.getResource(obj)
if err != nil {
return err
@@ -181,6 +189,7 @@ func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object
result := r.Get().
NamespaceIfScoped(key.Namespace, r.isNamespaced()).
Resource(r.resource()).
VersionedParams(getOpts.AsGetOptions(), uc.paramCodec).
Name(key.Name).
Do(ctx).
Into(obj)
@@ -200,14 +209,14 @@ func (uc *unstructuredClient) List(ctx context.Context, obj ObjectList, opts ...
gvk := u.GroupVersionKind()
gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
listOpts := ListOptions{}
listOpts.ApplyOptions(opts)
r, err := uc.cache.getResource(obj)
if err != nil {
return err
}
listOpts := ListOptions{}
listOpts.ApplyOptions(opts)
return r.Get().
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
Resource(r.resource()).
@@ -216,7 +225,70 @@ func (uc *unstructuredClient) List(ctx context.Context, obj ObjectList, opts ...
Into(obj)
}
func (uc *unstructuredClient) UpdateStatus(ctx context.Context, obj Object, opts ...UpdateOption) error {
func (uc *unstructuredClient) GetSubResource(ctx context.Context, obj, subResourceObj Object, subResource string, opts ...SubResourceGetOption) error {
if _, ok := obj.(*unstructured.Unstructured); !ok {
return fmt.Errorf("unstructured client did not understand object: %T", subResource)
}
if _, ok := subResourceObj.(*unstructured.Unstructured); !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
if subResourceObj.GetName() == "" {
subResourceObj.SetName(obj.GetName())
}
o, err := uc.cache.getObjMeta(obj)
if err != nil {
return err
}
getOpts := &SubResourceGetOptions{}
getOpts.ApplyOptions(opts)
return o.Get().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource(subResource).
VersionedParams(getOpts.AsGetOptions(), uc.paramCodec).
Do(ctx).
Into(subResourceObj)
}
func (uc *unstructuredClient) CreateSubResource(ctx context.Context, obj, subResourceObj Object, subResource string, opts ...SubResourceCreateOption) error {
if _, ok := obj.(*unstructured.Unstructured); !ok {
return fmt.Errorf("unstructured client did not understand object: %T", subResourceObj)
}
if _, ok := subResourceObj.(*unstructured.Unstructured); !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
if subResourceObj.GetName() == "" {
subResourceObj.SetName(obj.GetName())
}
o, err := uc.cache.getObjMeta(obj)
if err != nil {
return err
}
createOpts := &SubResourceCreateOptions{}
createOpts.ApplyOptions(opts)
return o.Post().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource(subResource).
Body(subResourceObj).
VersionedParams(createOpts.AsCreateOptions(), uc.paramCodec).
Do(ctx).
Into(subResourceObj)
}
func (uc *unstructuredClient) UpdateSubResource(ctx context.Context, obj Object, subResource string, opts ...SubResourceUpdateOption) error {
if _, ok := obj.(*unstructured.Unstructured); !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
@@ -226,18 +298,32 @@ func (uc *unstructuredClient) UpdateStatus(ctx context.Context, obj Object, opts
return err
}
updateOpts := SubResourceUpdateOptions{}
updateOpts.ApplyOptions(opts)
body := obj
if updateOpts.SubResourceBody != nil {
body = updateOpts.SubResourceBody
}
if body.GetName() == "" {
body.SetName(obj.GetName())
}
if body.GetNamespace() == "" {
body.SetNamespace(obj.GetNamespace())
}
return o.Put().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource("status").
Body(obj).
VersionedParams((&UpdateOptions{}).ApplyOptions(opts).AsUpdateOptions(), uc.paramCodec).
SubResource(subResource).
Body(body).
VersionedParams(updateOpts.AsUpdateOptions(), uc.paramCodec).
Do(ctx).
Into(obj)
Into(body)
}
func (uc *unstructuredClient) PatchStatus(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
func (uc *unstructuredClient) PatchSubResource(ctx context.Context, obj Object, subResource string, patch Patch, opts ...SubResourcePatchOption) error {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
@@ -250,21 +336,28 @@ func (uc *unstructuredClient) PatchStatus(ctx context.Context, obj Object, patch
return err
}
data, err := patch.Data(obj)
patchOpts := &SubResourcePatchOptions{}
patchOpts.ApplyOptions(opts)
body := obj
if patchOpts.SubResourceBody != nil {
body = patchOpts.SubResourceBody
}
data, err := patch.Data(body)
if err != nil {
return err
}
patchOpts := &PatchOptions{}
result := o.Patch(patch.Type()).
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource("status").
SubResource(subResource).
Body(data).
VersionedParams(patchOpts.ApplyOptions(opts).AsPatchOptions(), uc.paramCodec).
VersionedParams(patchOpts.AsPatchOptions(), uc.paramCodec).
Do(ctx).
Into(u)
Into(body)
u.SetGroupVersionKind(gvk)
return result

View File

@@ -112,6 +112,7 @@ type Options struct {
// NewClient is the func that creates the client to be used by the manager.
// If not set this will create the default DelegatingClient that will
// use the cache for reads and the client for writes.
// NOTE: The default client will not cache Unstructured.
NewClient NewClientFunc
// ClientDisableCacheFor tells the client that, if any cache is used, to bypass it
@@ -255,16 +256,33 @@ func setOptionsDefaults(options Options) Options {
// NewClientFunc allows a user to define how to create a client.
type NewClientFunc func(cache cache.Cache, config *rest.Config, options client.Options, uncachedObjects ...client.Object) (client.Client, error)
// DefaultNewClient creates the default caching client.
func DefaultNewClient(cache cache.Cache, config *rest.Config, options client.Options, uncachedObjects ...client.Object) (client.Client, error) {
c, err := client.New(config, options)
if err != nil {
return nil, err
}
return client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cache,
Client: c,
UncachedObjects: uncachedObjects,
})
// ClientOptions are the optional arguments for tuning the caching client.
type ClientOptions struct {
UncachedObjects []client.Object
CacheUnstructured bool
}
// DefaultNewClient creates the default caching client, that will never cache Unstructured.
func DefaultNewClient(cache cache.Cache, config *rest.Config, options client.Options, uncachedObjects ...client.Object) (client.Client, error) {
return ClientBuilderWithOptions(ClientOptions{})(cache, config, options, uncachedObjects...)
}
// ClientBuilderWithOptions returns a Client constructor that will build a client
// honoring the options argument
func ClientBuilderWithOptions(options ClientOptions) NewClientFunc {
return func(cache cache.Cache, config *rest.Config, clientOpts client.Options, uncachedObjects ...client.Object) (client.Client, error) {
options.UncachedObjects = append(options.UncachedObjects, uncachedObjects...)
c, err := client.New(config, clientOpts)
if err != nil {
return nil, err
}
return client.NewDelegatingClient(client.NewDelegatingClientInput{
CacheReader: cache,
Client: c,
UncachedObjects: options.UncachedObjects,
CacheUnstructured: options.CacheUnstructured,
})
}
}

View File

@@ -24,20 +24,24 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" //nolint:staticcheck
)
// ControllerManagerConfiguration defines the functions necessary to parse a config file
// and to configure the Options struct for the ctrl.Manager.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
type ControllerManagerConfiguration interface {
runtime.Object
// Complete returns the versioned configuration
Complete() (v1alpha1.ControllerManagerConfigurationSpec, error)
Complete() (v1alpha1.ControllerManagerConfigurationSpec, error) //nolint:staticcheck
}
// DeferredFileLoader is used to configure the decoder for loading controller
// runtime component config types.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
type DeferredFileLoader struct {
ControllerManagerConfiguration
path string
@@ -50,8 +54,10 @@ type DeferredFileLoader struct {
// this will also configure the defaults for the loader if nothing is
//
// Defaults:
// Path: "./config.yaml"
// Kind: GenericControllerManagerConfiguration
// * Path: "./config.yaml"
// * Kind: GenericControllerManagerConfiguration
//
// Deprecated: This package has been deprecated and will be removed in a future release.
func File() *DeferredFileLoader {
scheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(scheme))
@@ -63,6 +69,8 @@ func File() *DeferredFileLoader {
}
// Complete will use sync.Once to set the scheme.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
func (d *DeferredFileLoader) Complete() (v1alpha1.ControllerManagerConfigurationSpec, error) {
d.once.Do(d.loadFile)
if d.err != nil {
@@ -71,25 +79,33 @@ func (d *DeferredFileLoader) Complete() (v1alpha1.ControllerManagerConfiguration
return d.ControllerManagerConfiguration.Complete()
}
// AtPath will set the path to load the file for the decoder.
// AtPath will set the path to load the file for the decoder
//
// Deprecated: This package has been deprecated and will be removed in a future release.
func (d *DeferredFileLoader) AtPath(path string) *DeferredFileLoader {
d.path = path
return d
}
// OfKind will set the type to be used for decoding the file into.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
func (d *DeferredFileLoader) OfKind(obj ControllerManagerConfiguration) *DeferredFileLoader {
d.ControllerManagerConfiguration = obj
return d
}
// InjectScheme will configure the scheme to be used for decoding the file.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
func (d *DeferredFileLoader) InjectScheme(scheme *runtime.Scheme) error {
d.scheme = scheme
return nil
}
// loadFile is used from the mutex.Once to load the file.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
func (d *DeferredFileLoader) loadFile() {
if d.scheme == nil {
d.err = fmt.Errorf("scheme not supplied to controller configuration loader")

View File

@@ -17,9 +17,11 @@ limitations under the License.
// Package config contains functionality for interacting with ComponentConfig
// files
//
// DeferredFileLoader
// # DeferredFileLoader
//
// This uses a deferred file decoding allowing you to chain your configuration
// setup. You can pass this into manager.Options#File and it will load your
// config.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
package config

View File

@@ -17,4 +17,6 @@ limitations under the License.
// Package v1alpha1 provides the ControllerManagerConfiguration used for
// configuring ctrl.Manager
// +kubebuilder:object:generate=true
//
// Deprecated: This package has been deprecated and will be removed in a future release.
package v1alpha1

View File

@@ -23,12 +23,18 @@ import (
var (
// GroupVersion is group version used to register these objects.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
GroupVersion = schema.GroupVersion{Group: "controller-runtime.sigs.k8s.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -25,6 +25,8 @@ import (
)
// ControllerManagerConfigurationSpec defines the desired state of GenericControllerManagerConfiguration.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
type ControllerManagerConfigurationSpec struct {
// SyncPeriod determines the minimum frequency at which watched resources are
// reconciled. A lower period will correct entropy more quickly, but reduce
@@ -75,6 +77,8 @@ type ControllerManagerConfigurationSpec struct {
// ControllerConfigurationSpec defines the global configuration for
// controllers registered with the manager.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
type ControllerConfigurationSpec struct {
// GroupKindConcurrency is a map from a Kind to the number of concurrent reconciliation
// allowed for that controller.
@@ -94,6 +98,10 @@ type ControllerConfigurationSpec struct {
// Defaults to 2 minutes if not set.
// +optional
CacheSyncTimeout *time.Duration `json:"cacheSyncTimeout,omitempty"`
// RecoverPanic indicates if panics should be recovered.
// +optional
RecoverPanic *bool `json:"recoverPanic,omitempty"`
}
// ControllerMetrics defines the metrics configs.
@@ -109,6 +117,7 @@ type ControllerMetrics struct {
type ControllerHealth struct {
// HealthProbeBindAddress is the TCP address that the controller should bind to
// for serving health probes
// It can be set to "0" or "" to disable serving the health probe.
// +optional
HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"`
@@ -144,14 +153,20 @@ type ControllerWebhook struct {
// +kubebuilder:object:root=true
// ControllerManagerConfiguration is the Schema for the GenericControllerManagerConfigurations API.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
type ControllerManagerConfiguration struct {
metav1.TypeMeta `json:",inline"`
// ControllerManagerConfiguration returns the contfigurations for controllers
//
// Deprecated: This package has been deprecated and will be removed in a future release.
ControllerManagerConfigurationSpec `json:",inline"`
}
// Complete returns the configuration for controller-runtime.
//
// Deprecated: This package has been deprecated and will be removed in a future release.
func (c *ControllerManagerConfigurationSpec) Complete() (ControllerManagerConfigurationSpec, error) {
return *c, nil
}

View File

@@ -27,6 +27,11 @@ func (in *ControllerConfigurationSpec) DeepCopyInto(out *ControllerConfiguration
*out = new(timex.Duration)
**out = **in
}
if in.RecoverPanic != nil {
in, out := &in.RecoverPanic, &out.RecoverPanic
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerConfigurationSpec.

View File

@@ -56,7 +56,8 @@ type Options struct {
CacheSyncTimeout time.Duration
// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
RecoverPanic bool
// Defaults to the Controller.RecoverPanic setting from the Manager if unset.
RecoverPanic *bool
}
// Controller implements a Kubernetes API. A Controller manages a work queue fed reconcile.Requests
@@ -139,6 +140,10 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
return nil, err
}
if options.RecoverPanic == nil {
options.RecoverPanic = mgr.GetControllerOptions().RecoverPanic //nolint:staticcheck
}
// Create controller with dependencies set
return &controller.Controller{
Do: options.Reconciler,
@@ -153,3 +158,6 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
RecoverPanic: options.RecoverPanic,
}, nil
}
// ReconcileIDFromContext gets the reconcileID from the current context.
var ReconcileIDFromContext = controller.ReconcileIDFromContext

View File

@@ -76,8 +76,8 @@ func SetControllerReference(owner, controlled metav1.Object, scheme *runtime.Sch
Kind: gvk.Kind,
Name: owner.GetName(),
UID: owner.GetUID(),
BlockOwnerDeletion: pointer.BoolPtr(true),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.Bool(true),
Controller: pointer.Bool(true),
}
// Return early with an error if the object is already controlled.
@@ -207,7 +207,7 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f M
return OperationResultCreated, nil
}
existing := obj.DeepCopyObject() //nolint
existing := obj.DeepCopyObject()
if err := mutate(f, key, obj); err != nil {
return OperationResultNone, err
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
/*
Package controller provides types and functions for building Controllers. Controllers implement Kubernetes APIs.
Creation
# Creation
To create a new Controller, first create a manager.Manager and pass it to the controller.New function.
The Controller MUST be started by calling Manager.Start.

View File

@@ -84,8 +84,10 @@ type CRDInstallOptions struct {
WebhookOptions WebhookInstallOptions
}
const defaultPollInterval = 100 * time.Millisecond
const defaultMaxWait = 10 * time.Second
const (
defaultPollInterval = 100 * time.Millisecond
defaultMaxWait = 10 * time.Second
)
// InstallCRDs installs a collection of CRDs into a cluster by reading the crd yaml files from a directory.
func InstallCRDs(config *rest.Config, options CRDInstallOptions) ([]*apiextensionsv1.CustomResourceDefinition, error) {
@@ -142,7 +144,7 @@ func defaultCRDOptions(o *CRDInstallOptions) {
// WaitForCRDs waits for the CRDs to appear in discovery.
func WaitForCRDs(config *rest.Config, crds []*apiextensionsv1.CustomResourceDefinition, options CRDInstallOptions) error {
// Add each CRD to a map of GroupVersion to Resource
waitingFor := map[schema.GroupVersion]*sets.String{}
waitingFor := map[schema.GroupVersion]*sets.Set[string]{}
for _, crd := range crds {
gvs := []schema.GroupVersion{}
for _, version := range crd.Spec.Versions {
@@ -155,7 +157,7 @@ func WaitForCRDs(config *rest.Config, crds []*apiextensionsv1.CustomResourceDefi
log.V(1).Info("adding API in waitlist", "GV", gv)
if _, found := waitingFor[gv]; !found {
// Initialize the set
waitingFor[gv] = &sets.String{}
waitingFor[gv] = &sets.Set[string]{}
}
// Add the Resource
waitingFor[gv].Insert(crd.Spec.Names.Plural)
@@ -173,7 +175,7 @@ type poller struct {
config *rest.Config
// waitingFor is the map of resources keyed by group version that have not yet been found in discovery
waitingFor map[schema.GroupVersion]*sets.String
waitingFor map[schema.GroupVersion]*sets.Set[string]
}
// poll checks if all the resources have been found in discovery, and returns false if not.
@@ -362,7 +364,7 @@ func modifyConversionWebhooks(crds []*apiextensionsv1.CustomResourceDefinition,
if err != nil {
return err
}
url := pointer.StringPtr(fmt.Sprintf("https://%s/convert", hostPort))
url := pointer.String(fmt.Sprintf("https://%s/convert", hostPort))
for i := range crds {
// Continue if we're preserving unknown fields.

View File

@@ -1,53 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package printer contains setup for a friendlier Ginkgo printer that's easier
// to parse by test automation.
package printer
import (
"fmt"
"github.com/onsi/ginkgo"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/types"
)
var _ ginkgo.Reporter = NewlineReporter{}
// NewlineReporter is Reporter that Prints a newline after the default Reporter output so that the results
// are correctly parsed by test automation.
// See issue https://github.com/jstemmer/go-junit-report/issues/31
type NewlineReporter struct{}
// SpecSuiteWillBegin implements ginkgo.Reporter.
func (NewlineReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) {
}
// BeforeSuiteDidRun implements ginkgo.Reporter.
func (NewlineReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {}
// AfterSuiteDidRun implements ginkgo.Reporter.
func (NewlineReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {}
// SpecWillRun implements ginkgo.Reporter.
func (NewlineReporter) SpecWillRun(specSummary *types.SpecSummary) {}
// SpecDidComplete implements ginkgo.Reporter.
func (NewlineReporter) SpecDidComplete(specSummary *types.SpecSummary) {}
// SpecSuiteDidEnd Prints a newline between "35 Passed | 0 Failed | 0 Pending | 0 Skipped" and "--- PASS:".
func (NewlineReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { fmt.Printf("\n") }

View File

@@ -1,109 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package printer
import (
"fmt"
"os"
"path/filepath"
"sync"
"github.com/onsi/ginkgo"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/reporters"
"github.com/onsi/ginkgo/types"
"k8s.io/apimachinery/pkg/util/sets"
)
var (
allRegisteredSuites = sets.String{}
allRegisteredSuitesLock = &sync.Mutex{}
)
type prowReporter struct {
junitReporter *reporters.JUnitReporter
}
// NewProwReporter returns a prowReporter that will write out junit if running in Prow and do
// nothing otherwise.
// WARNING: It seems this does not always properly fail the test runs when there are failures,
// see https://github.com/onsi/ginkgo/issues/706
// When using this you must make sure to grep for failures in your junit xmls and fail the run
// if there are any.
func NewProwReporter(suiteName string) ginkgo.Reporter {
allRegisteredSuitesLock.Lock()
if allRegisteredSuites.Has(suiteName) {
panic(fmt.Sprintf("Suite named %q registered more than once", suiteName))
}
allRegisteredSuites.Insert(suiteName)
allRegisteredSuitesLock.Unlock()
if os.Getenv("CI") == "" {
return &prowReporter{}
}
artifactsDir := os.Getenv("ARTIFACTS")
if artifactsDir == "" {
return &prowReporter{}
}
path := filepath.Join(artifactsDir, fmt.Sprintf("junit_%s_%d.xml", suiteName, config.GinkgoConfig.ParallelNode))
return &prowReporter{
junitReporter: reporters.NewJUnitReporter(path),
}
}
func (pr *prowReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) {
if pr.junitReporter != nil {
pr.junitReporter.SpecSuiteWillBegin(config, summary)
}
}
// BeforeSuiteDidRun implements ginkgo.Reporter.
func (pr *prowReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {
if pr.junitReporter != nil {
pr.junitReporter.BeforeSuiteDidRun(setupSummary)
}
}
// AfterSuiteDidRun implements ginkgo.Reporter.
func (pr *prowReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {
if pr.junitReporter != nil {
pr.junitReporter.AfterSuiteDidRun(setupSummary)
}
}
// SpecWillRun implements ginkgo.Reporter.
func (pr *prowReporter) SpecWillRun(specSummary *types.SpecSummary) {
if pr.junitReporter != nil {
pr.junitReporter.SpecWillRun(specSummary)
}
}
// SpecDidComplete implements ginkgo.Reporter.
func (pr *prowReporter) SpecDidComplete(specSummary *types.SpecSummary) {
if pr.junitReporter != nil {
pr.junitReporter.SpecDidComplete(specSummary)
}
}
// SpecSuiteDidEnd Prints a newline between "35 Passed | 0 Failed | 0 Pending | 0 Skipped" and "--- PASS:".
func (pr *prowReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) {
if pr.junitReporter != nil {
pr.junitReporter.SpecSuiteDidEnd(summary)
}
}

View File

@@ -37,15 +37,14 @@ var log = logf.RuntimeLog.WithName("test-env")
/*
It's possible to override some defaults, by setting the following environment variables:
USE_EXISTING_CLUSTER (boolean): if set to true, envtest will use an existing cluster
TEST_ASSET_KUBE_APISERVER (string): path to the api-server binary to use
TEST_ASSET_ETCD (string): path to the etcd binary to use
TEST_ASSET_KUBECTL (string): path to the kubectl binary to use
KUBEBUILDER_ASSETS (string): directory containing the binaries to use (api-server, etcd and kubectl). Defaults to /usr/local/kubebuilder/bin.
KUBEBUILDER_CONTROLPLANE_START_TIMEOUT (string supported by time.ParseDuration): timeout for test control plane to start. Defaults to 20s.
KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT (string supported by time.ParseDuration): timeout for test control plane to start. Defaults to 20s.
KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT (boolean): if set to true, the control plane's stdout and stderr are attached to os.Stdout and os.Stderr
* USE_EXISTING_CLUSTER (boolean): if set to true, envtest will use an existing cluster
* TEST_ASSET_KUBE_APISERVER (string): path to the api-server binary to use
* TEST_ASSET_ETCD (string): path to the etcd binary to use
* TEST_ASSET_KUBECTL (string): path to the kubectl binary to use
* KUBEBUILDER_ASSETS (string): directory containing the binaries to use (api-server, etcd and kubectl). Defaults to /usr/local/kubebuilder/bin.
* KUBEBUILDER_CONTROLPLANE_START_TIMEOUT (string supported by time.ParseDuration): timeout for test control plane to start. Defaults to 20s.
* KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT (string supported by time.ParseDuration): timeout for test control plane to start. Defaults to 20s.
* KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT (boolean): if set to true, the control plane's stdout and stderr are attached to os.Stdout and os.Stderr
*/
const (
envUseExistingCluster = "USE_EXISTING_CLUSTER"

View File

@@ -173,7 +173,7 @@ func WaitForWebhooks(config *rest.Config,
mutatingWebhooks []*admissionv1.MutatingWebhookConfiguration,
validatingWebhooks []*admissionv1.ValidatingWebhookConfiguration,
options WebhookInstallOptions) error {
waitingFor := map[schema.GroupVersionKind]*sets.String{}
waitingFor := map[schema.GroupVersionKind]*sets.Set[string]{}
for _, hook := range mutatingWebhooks {
h := hook
@@ -183,7 +183,7 @@ func WaitForWebhooks(config *rest.Config,
}
if _, ok := waitingFor[gvk]; !ok {
waitingFor[gvk] = &sets.String{}
waitingFor[gvk] = &sets.Set[string]{}
}
waitingFor[gvk].Insert(h.GetName())
}
@@ -196,7 +196,7 @@ func WaitForWebhooks(config *rest.Config,
}
if _, ok := waitingFor[gvk]; !ok {
waitingFor[gvk] = &sets.String{}
waitingFor[gvk] = &sets.Set[string]{}
}
waitingFor[gvk].Insert(hook.GetName())
}
@@ -212,7 +212,7 @@ type webhookPoller struct {
config *rest.Config
// waitingFor is the map of resources keyed by group version that have not yet been found in discovery
waitingFor map[schema.GroupVersionKind]*sets.String
waitingFor map[schema.GroupVersionKind]*sets.Set[string]
}
// poll checks if all the resources have been found in discovery, and returns false if not.
@@ -229,7 +229,7 @@ func (p *webhookPoller) poll() (done bool, err error) {
delete(p.waitingFor, gvk)
continue
}
for _, name := range names.List() {
for _, name := range names.UnsortedList() {
var obj = &unstructured.Unstructured{}
obj.SetGroupVersionKind(gvk)
err := c.Get(context.Background(), client.ObjectKey{

View File

@@ -21,7 +21,7 @@ Controller.Watch in order to generate and enqueue reconcile.Request work items.
Generally, following premade event handlers should be sufficient for most use cases:
EventHandlers
EventHandlers:
EnqueueRequestForObject - Enqueues a reconcile.Request containing the Name and Namespace of the object in the Event. This will
cause the object that was the source of the Event (e.g. the created / deleted / updated object) to be

View File

@@ -70,7 +70,7 @@ func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) {
parts = append(parts, checkStatus{name: "ping", healthy: true})
}
for _, c := range excluded.List() {
for _, c := range excluded.UnsortedList() {
log.V(1).Info("cannot exclude health check, no matches for it", "checker", c)
}
@@ -88,7 +88,7 @@ func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) {
// any checks that the user requested to have excluded, but weren't actually
// known checks. writeStatusAsText is always verbose on failure, and can be
// forced to be verbose on success using the given argument.
func writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownExcludes sets.String, failed, forceVerbose bool) {
func writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownExcludes sets.Set[string], failed, forceVerbose bool) {
resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
resp.Header().Set("X-Content-Type-Options", "nosniff")
@@ -121,7 +121,7 @@ func writeStatusesAsText(resp http.ResponseWriter, parts []checkStatus, unknownE
}
if unknownExcludes.Len() > 0 {
fmt.Fprintf(resp, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(unknownExcludes.List()...))
fmt.Fprintf(resp, "warn: some health checks cannot be excluded: no matches for %s\n", formatQuoted(unknownExcludes.UnsortedList()...))
}
if failed {
@@ -187,12 +187,12 @@ type Checker func(req *http.Request) error
var Ping Checker = func(_ *http.Request) error { return nil }
// getExcludedChecks extracts the health check names to be excluded from the query param.
func getExcludedChecks(r *http.Request) sets.String {
func getExcludedChecks(r *http.Request) sets.Set[string] {
checks, found := r.URL.Query()["exclude"]
if found {
return sets.NewString(checks...)
return sets.New[string](checks...)
}
return sets.NewString()
return sets.New[string]()
}
// formatQuoted returns a formatted string of the health check names,

View File

@@ -24,6 +24,7 @@ import (
"time"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/client-go/util/workqueue"
@@ -91,7 +92,7 @@ type Controller struct {
LogConstructor func(request *reconcile.Request) logr.Logger
// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
RecoverPanic bool
RecoverPanic *bool
}
// watchDescription contains all the information necessary to start a watch.
@@ -105,7 +106,7 @@ type watchDescription struct {
func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) {
defer func() {
if r := recover(); r != nil {
if c.RecoverPanic {
if c.RecoverPanic != nil && *c.RecoverPanic {
for _, fn := range utilruntime.PanicHandlers {
fn(r)
}
@@ -311,9 +312,11 @@ func (c *Controller) reconcileHandler(ctx context.Context, obj interface{}) {
}
log := c.LogConstructor(&req)
reconcileID := uuid.NewUUID()
log = log.WithValues("reconcileID", uuid.NewUUID())
log = log.WithValues("reconcileID", reconcileID)
ctx = logf.IntoContext(ctx, log)
ctx = addReconcileID(ctx, reconcileID)
// RunInformersAndControllers the syncHandler, passing it the Namespace/Name string of the
// resource to be synced.
@@ -358,3 +361,21 @@ func (c *Controller) InjectFunc(f inject.Func) error {
func (c *Controller) updateMetrics(reconcileTime time.Duration) {
ctrlmetrics.ReconcileTime.WithLabelValues(c.Name).Observe(reconcileTime.Seconds())
}
// ReconcileIDFromContext gets the reconcileID from the current context.
func ReconcileIDFromContext(ctx context.Context) types.UID {
r, ok := ctx.Value(reconcileIDKey{}).(types.UID)
if !ok {
return ""
}
return r
}
// reconcileIDKey is a context.Context Value key. Its associated value should
// be a types.UID.
type reconcileIDKey struct{}
func addReconcileID(ctx context.Context, reconcileID types.UID) context.Context {
return context.WithValue(ctx, reconcileIDKey{}, reconcileID)
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package selector
import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/selection"
)
// 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
}

View File

@@ -100,7 +100,7 @@ func (p *Provider) getBroadcaster() record.EventBroadcaster {
broadcaster.StartRecordingToSink(&corev1client.EventSinkImpl{Interface: p.evtClient})
broadcaster.StartEventWatcher(
func(e *corev1.Event) {
p.logger.V(1).Info(e.Type, "object", e.InvolvedObject, "reason", e.Reason, "message", e.Message)
p.logger.V(1).Info(e.Message, "type", e.Type, "object", e.InvolvedObject, "reason", e.Reason)
})
p.broadcaster = broadcaster
p.stopBroadcaster = stop

View File

@@ -71,10 +71,20 @@ func (c *portCache) add(port int) (bool, error) {
}
info, err := d.Info()
if err != nil {
// No-op if file no longer exists; may have been deleted by another
// process/thread trying to allocate ports.
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}
if time.Since(info.ModTime()) > portReserveTime {
if err := os.Remove(filepath.Join(cacheDir, path)); err != nil {
// No-op if file no longer exists; may have been deleted by another
// process/thread trying to allocate ports.
if os.IsNotExist(err) {
return nil
}
return err
}
}

View File

@@ -93,12 +93,12 @@ type TemplateDefaults struct {
// TemplateAndArguments joins structured arguments and non-structured arguments, preserving existing
// behavior. Namely:
//
// 1. if templ has len > 0, it will be rendered against data
// 2. the rendered template values that look like `--foo=bar` will be split
// and appended to args, the rest will be kept around
// 3. the given args will be rendered as string form. If a template is given,
// no defaults will be used, otherwise defaults will be used
// 4. a result of [args..., rest...] will be returned
// 1. if templ has len > 0, it will be rendered against data
// 2. the rendered template values that look like `--foo=bar` will be split
// and appended to args, the rest will be kept around
// 3. the given args will be rendered as string form. If a template is given,
// no defaults will be used, otherwise defaults will be used
// 4. a result of [args..., rest...] will be returned
//
// It returns the resulting rendered arguments, plus the arguments that were
// not transferred to `args` during rendering.
@@ -215,9 +215,9 @@ var (
// for passing to exec.Command and friends, making use of the given defaults
// as indicated for each particular argument.
//
// - Any flag in defaults that's not in Arguments will be present in the output
// - Any flag that's present in Arguments will be passed the corresponding
// defaults to do with as it will (ignore, append-to, suppress, etc).
// - Any flag in defaults that's not in Arguments will be present in the output
// - Any flag that's present in Arguments will be passed the corresponding
// defaults to do with as it will (ignore, append-to, suppress, etc).
func (a *Arguments) AsStrings(defaults map[string][]string) []string {
// sort for deterministic ordering
keysInOrder := make([]string, 0, len(defaults)+len(a.values))
@@ -323,9 +323,9 @@ func (a *Arguments) SetRaw(key string, val Arg) *Arguments {
// used in conjunction with SetRaw. For example, to set `--some-flag` to the
// API server's CertDir, you could do:
//
// server.Configure().SetRaw("--some-flag", FuncArg(func(defaults []string) []string {
// return []string{server.CertDir}
// }))
// server.Configure().SetRaw("--some-flag", FuncArg(func(defaults []string) []string {
// return []string{server.CertDir}
// }))
//
// FuncArg ignores Appends; if you need to support appending values too, consider implementing
// Arg directly.

View File

@@ -184,16 +184,12 @@ func (ps *State) Start(stdout, stderr io.Writer) (err error) {
ps.ready = true
return nil
case <-ps.waitDone:
if pollerStopCh != nil {
close(pollerStopCh)
}
close(pollerStopCh)
return fmt.Errorf("timeout waiting for process %s to start successfully "+
"(it may have failed to start, or stopped unexpectedly before becoming ready)",
path.Base(ps.Path))
case <-timedOut:
if pollerStopCh != nil {
close(pollerStopCh)
}
close(pollerStopCh)
if ps.Cmd != nil {
// intentionally ignore this -- we might've crashed, failed to start, etc
ps.Cmd.Process.Signal(syscall.SIGTERM) //nolint:errcheck

View File

@@ -73,6 +73,9 @@ func (p *loggerPromise) Fulfill(parentLogSink logr.LogSink) {
p.logger.lock.Lock()
p.logger.logger = sink
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
p.logger.logger = withCallDepth.WithCallDepth(1)
}
p.logger.promise = nil
p.logger.lock.Unlock()
@@ -141,7 +144,11 @@ func (l *DelegatingLogSink) WithName(name string) logr.LogSink {
defer l.lock.RUnlock()
if l.promise == nil {
return l.logger.WithName(name)
sink := l.logger.WithName(name)
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
sink = withCallDepth.WithCallDepth(-1)
}
return sink
}
res := &DelegatingLogSink{logger: l.logger}
@@ -157,7 +164,11 @@ func (l *DelegatingLogSink) WithValues(tags ...interface{}) logr.LogSink {
defer l.lock.RUnlock()
if l.promise == nil {
return l.logger.WithValues(tags...)
sink := l.logger.WithValues(tags...)
if withCallDepth, ok := sink.(logr.CallDepthLogSink); ok {
sink = withCallDepth.WithCallDepth(-1)
}
return sink
}
res := &DelegatingLogSink{logger: l.logger}

View File

@@ -17,7 +17,7 @@ limitations under the License.
// Package log contains utilities for fetching a new logger
// when one is not already available.
//
// The Log Handle
// # The Log Handle
//
// This package contains a root logr.Logger Log. It may be used to
// get a handle to whatever the root logging implementation is. By
@@ -25,7 +25,7 @@ limitations under the License.
// to loggers. When the implementation is set using SetLogger, these
// "promises" will be converted over to real loggers.
//
// Logr
// # Logr
//
// All logging in controller-runtime is structured, using a set of interfaces
// defined by a package called logr

View File

@@ -18,6 +18,7 @@ package manager
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
@@ -40,7 +41,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" //nolint:staticcheck
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/internal/httpserver"
intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder"
@@ -107,7 +108,7 @@ type controllerManager struct {
healthzHandler *healthz.Handler
// controllerOptions are the global controller options.
controllerOptions v1alpha1.ControllerConfigurationSpec
controllerOptions v1alpha1.ControllerConfigurationSpec //nolint:staticcheck
// Logger is the logger that should be used by this manager.
// If none is set, it defaults to log.Log global logger.
@@ -135,12 +136,17 @@ type controllerManager struct {
// if not set, webhook server would look up the server key and certificate in
// {TempDir}/k8s-webhook-server/serving-certs
certDir string
// tlsOpts is used to allow configuring the TLS config used for the webhook server.
tlsOpts []func(*tls.Config)
webhookServer *webhook.Server
// webhookServerOnce will be called in GetWebhookServer() to optionally initialize
// webhookServer if unset, and Add() it to controllerManager.
webhookServerOnce sync.Once
// leaderElectionID is the name of the resource that leader election
// will use for holding the leader lock.
leaderElectionID string
// leaseDuration is the duration that non-leader candidates will
// wait to force acquire leadership.
leaseDuration time.Duration
@@ -305,6 +311,7 @@ func (cm *controllerManager) GetWebhookServer() *webhook.Server {
Port: cm.port,
Host: cm.host,
CertDir: cm.certDir,
TLSOpts: cm.tlsOpts,
}
}
if err := cm.Add(cm.webhookServer); err != nil {
@@ -318,7 +325,7 @@ func (cm *controllerManager) GetLogger() logr.Logger {
return cm.logger
}
func (cm *controllerManager) GetControllerOptions() v1alpha1.ControllerConfigurationSpec {
func (cm *controllerManager) GetControllerOptions() v1alpha1.ControllerConfigurationSpec { //nolint:staticcheck
return cm.controllerOptions
}
@@ -402,6 +409,8 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
cm.Unlock()
return errors.New("manager already started")
}
cm.started = true
var ready bool
defer func() {
// Only unlock the manager if we haven't reached
@@ -633,6 +642,7 @@ func (cm *controllerManager) startLeaderElection(ctx context.Context) (err error
},
},
ReleaseOnCancel: cm.leaderElectionReleaseOnCancel,
Name: cm.leaderElectionID,
})
if err != nil {
return err

View File

@@ -18,6 +18,7 @@ package manager
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
@@ -35,8 +36,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"
"sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/config" //nolint:staticcheck
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" //nolint:staticcheck
"sigs.k8s.io/controller-runtime/pkg/healthz"
intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder"
"sigs.k8s.io/controller-runtime/pkg/leaderelection"
@@ -93,7 +94,11 @@ type Manager interface {
GetLogger() logr.Logger
// GetControllerOptions returns controller global configuration options.
GetControllerOptions() v1alpha1.ControllerConfigurationSpec
//
// Deprecated: In a future version, the returned value is going to be replaced with a
// different type that doesn't rely on component configuration types.
// This is a temporary warning, and no action is needed as of today.
GetControllerOptions() v1alpha1.ControllerConfigurationSpec //nolint:staticcheck
}
// Options are the arguments for creating a new Manager.
@@ -142,18 +147,36 @@ type Options struct {
LeaderElection bool
// LeaderElectionResourceLock determines which resource lock to use for leader election,
// defaults to "configmapsleases". Change this value only if you know what you are doing.
// Otherwise, users of your controller might end up with multiple running instances that
// defaults to "leases". Change this value only if you know what you are doing.
//
// If you are using `configmaps`/`endpoints` resource lock and want to migrate to "leases",
// you might do so by migrating to the respective multilock first ("configmapsleases" or "endpointsleases"),
// which will acquire a leader lock on both resources.
// After all your users have migrated to the multilock, you can go ahead and migrate to "leases".
// Please also keep in mind, that users might skip versions of your controller.
//
// Note: before controller-runtime version v0.7, it was set to "configmaps".
// And from v0.7 to v0.11, the default was "configmapsleases", which was
// used to migrate from configmaps to leases.
// Since the default was "configmapsleases" for over a year, spanning five minor releases,
// any actively maintained operators are very likely to have a released version that uses
// "configmapsleases". Therefore defaulting to "leases" should be safe since v0.12.
//
// So, what do you have to do when you are updating your controller-runtime dependency
// from a lower version to v0.12 or newer?
// - If your operator matches at least one of these conditions:
// - the LeaderElectionResourceLock in your operator has already been explicitly set to "leases"
// - the old controller-runtime version is between v0.7.0 and v0.11.x and the
// LeaderElectionResourceLock wasn't set or was set to "leases"/"configmapsleases"/"endpointsleases"
// feel free to update controller-runtime to v0.12 or newer.
// - Otherwise, you may have to take these steps:
// 1. update controller-runtime to v0.12 or newer in your go.mod
// 2. set LeaderElectionResourceLock to "configmapsleases" (or "endpointsleases")
// 3. package your operator and upgrade it in all your clusters
// 4. only if you have finished 3, you can remove the LeaderElectionResourceLock to use the default "leases"
// Otherwise, your operator might end up with multiple running instances that
// each acquired leadership through different resource locks during upgrades and thus
// act on the same resources concurrently.
// If you want to migrate to the "leases" resource lock, you might do so by migrating to the
// respective multilock first ("configmapsleases" or "endpointsleases"), which will acquire a
// leader lock on both resources. After all your users have migrated to the multilock, you can
// go ahead and migrate to "leases". Please also keep in mind, that users might skip versions
// of your controller.
//
// Note: before controller-runtime version v0.7, the resource lock was set to "configmaps".
// Please keep this in mind, when planning a proper migration path for your controller.
LeaderElectionResourceLock string
// LeaderElectionNamespace determines the namespace in which the leader
@@ -175,6 +198,12 @@ type Options struct {
// LeaseDuration time first.
LeaderElectionReleaseOnCancel bool
// LeaderElectionResourceLockInterface allows to provide a custom resourcelock.Interface that was created outside
// of the controller-runtime. If this value is set the options LeaderElectionID, LeaderElectionNamespace,
// LeaderElectionResourceLock, LeaseDuration, RenewDeadline and RetryPeriod will be ignored. This can be useful if you
// want to use a locking mechanism that is currently not supported, like a MultiLock across two Kubernetes clusters.
LeaderElectionResourceLockInterface resourcelock.Interface
// LeaseDuration is the duration that non-leader candidates will
// wait to force acquire leadership. This is measured against time of
// last observed ack. Default is 15 seconds.
@@ -201,6 +230,7 @@ type Options struct {
// HealthProbeBindAddress is the TCP address that the controller should bind to
// for serving health probes
// It can be set to "0" or "" to disable serving the health probe.
HealthProbeBindAddress string
// Readiness probe endpoint name, defaults to "readyz"
@@ -223,6 +253,9 @@ type Options struct {
// It is used to set webhook.Server.CertDir if WebhookServer is not set.
CertDir string
// TLSOpts is used to allow configuring the TLS config used for the webhook server.
TLSOpts []func(*tls.Config)
// WebhookServer is an externally configured webhook.Server. By default,
// a Manager will create a default server using Port, Host, and CertDir;
// if this is set, the Manager will use this server instead.
@@ -268,7 +301,11 @@ type Options struct {
// Controller contains global configuration options for controllers
// registered within this manager.
// +optional
Controller v1alpha1.ControllerConfigurationSpec
//
// Deprecated: In a future version, the type of this field is going to be replaced with a
// different struct that doesn't rely on component configuration types.
// This is a temporary warning, and no action is needed as of today.
Controller v1alpha1.ControllerConfigurationSpec //nolint:staticcheck
// makeBroadcaster allows deferring the creation of the broadcaster to
// avoid leaking goroutines if we never call Start on this manager. It also
@@ -358,14 +395,19 @@ func New(config *rest.Config, options Options) (Manager, error) {
}
}
resourceLock, err := options.newResourceLock(leaderConfig, leaderRecorderProvider, leaderelection.Options{
LeaderElection: options.LeaderElection,
LeaderElectionResourceLock: options.LeaderElectionResourceLock,
LeaderElectionID: options.LeaderElectionID,
LeaderElectionNamespace: options.LeaderElectionNamespace,
})
if err != nil {
return nil, err
var resourceLock resourcelock.Interface
if options.LeaderElectionResourceLockInterface != nil && options.LeaderElection {
resourceLock = options.LeaderElectionResourceLockInterface
} else {
resourceLock, err = options.newResourceLock(leaderConfig, leaderRecorderProvider, leaderelection.Options{
LeaderElection: options.LeaderElection,
LeaderElectionResourceLock: options.LeaderElectionResourceLock,
LeaderElectionID: options.LeaderElectionID,
LeaderElectionNamespace: options.LeaderElectionNamespace,
})
if err != nil {
return nil, err
}
}
// Create the metrics listener. This will throw an error if the metrics bind
@@ -403,7 +445,9 @@ func New(config *rest.Config, options Options) (Manager, error) {
port: options.Port,
host: options.Host,
certDir: options.CertDir,
tlsOpts: options.TLSOpts,
webhookServer: options.WebhookServer,
leaderElectionID: options.LeaderElectionID,
leaseDuration: *options.LeaseDuration,
renewDeadline: *options.RenewDeadline,
retryPeriod: *options.RetryPeriod,
@@ -420,6 +464,8 @@ func New(config *rest.Config, options Options) (Manager, error) {
// AndFrom will use a supplied type and convert to Options
// any options already set on Options will be ignored, this is used to allow
// cli flags to override anything specified in the config file.
//
// Deprecated: This method will be removed in a future release.
func (o Options) AndFrom(loader config.ControllerManagerConfiguration) (Options, error) {
if inj, wantsScheme := loader.(inject.Scheme); wantsScheme {
err := inj.InjectScheme(o.Scheme)
@@ -485,6 +531,8 @@ func (o Options) AndFrom(loader config.ControllerManagerConfiguration) (Options,
}
// AndFromOrDie will use options.AndFrom() and will panic if there are errors.
//
// Deprecated: This method will be removed in a future release.
func (o Options) AndFromOrDie(loader config.ControllerManagerConfiguration) Options {
o, err := o.AndFrom(loader)
if err != nil {
@@ -493,7 +541,7 @@ func (o Options) AndFromOrDie(loader config.ControllerManagerConfiguration) Opti
return o
}
func (o Options) setLeaderElectionConfig(obj v1alpha1.ControllerManagerConfigurationSpec) Options {
func (o Options) setLeaderElectionConfig(obj v1alpha1.ControllerManagerConfigurationSpec) Options { //nolint:staticcheck
if obj.LeaderElection == nil {
// The source does not have any configuration; noop
return o

View File

@@ -22,7 +22,6 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
reflectormetrics "k8s.io/client-go/tools/cache"
clientmetrics "k8s.io/client-go/tools/metrics"
)
@@ -37,19 +36,6 @@ const (
ResultKey = "requests_total"
)
// Metrics subsystem and all keys used by the reflectors.
const (
ReflectorSubsystem = "reflector"
ListsTotalKey = "lists_total"
ListsDurationKey = "list_duration_seconds"
ItemsPerListKey = "items_per_list"
WatchesTotalKey = "watches_total"
ShortWatchesTotalKey = "short_watches_total"
WatchDurationKey = "watch_duration_seconds"
ItemsPerWatchKey = "items_per_watch"
LastResourceVersionKey = "last_resource_version"
)
var (
// client metrics.
@@ -81,64 +67,10 @@ var (
Name: ResultKey,
Help: "Number of HTTP requests, partitioned by status code, method, and host.",
}, []string{"code", "method", "host"})
// reflector metrics.
// TODO(directxman12): update these to be histograms once the metrics overhaul KEP
// PRs start landing.
listsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: ReflectorSubsystem,
Name: ListsTotalKey,
Help: "Total number of API lists done by the reflectors",
}, []string{"name"})
listsDuration = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Subsystem: ReflectorSubsystem,
Name: ListsDurationKey,
Help: "How long an API list takes to return and decode for the reflectors",
}, []string{"name"})
itemsPerList = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Subsystem: ReflectorSubsystem,
Name: ItemsPerListKey,
Help: "How many items an API list returns to the reflectors",
}, []string{"name"})
watchesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: ReflectorSubsystem,
Name: WatchesTotalKey,
Help: "Total number of API watches done by the reflectors",
}, []string{"name"})
shortWatchesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: ReflectorSubsystem,
Name: ShortWatchesTotalKey,
Help: "Total number of short API watches done by the reflectors",
}, []string{"name"})
watchDuration = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Subsystem: ReflectorSubsystem,
Name: WatchDurationKey,
Help: "How long an API watch takes to return and decode for the reflectors",
}, []string{"name"})
itemsPerWatch = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Subsystem: ReflectorSubsystem,
Name: ItemsPerWatchKey,
Help: "How many items an API watch returns to the reflectors",
}, []string{"name"})
lastResourceVersion = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: ReflectorSubsystem,
Name: LastResourceVersionKey,
Help: "Last resource version seen for the reflectors",
}, []string{"name"})
)
func init() {
registerClientMetrics()
registerReflectorMetrics()
}
// registerClientMetrics sets up the client latency metrics from client-go.
@@ -152,20 +84,6 @@ func registerClientMetrics() {
})
}
// registerReflectorMetrics sets up reflector (reconcile) loop metrics.
func registerReflectorMetrics() {
Registry.MustRegister(listsTotal)
Registry.MustRegister(listsDuration)
Registry.MustRegister(itemsPerList)
Registry.MustRegister(watchesTotal)
Registry.MustRegister(shortWatchesTotal)
Registry.MustRegister(watchDuration)
Registry.MustRegister(itemsPerWatch)
Registry.MustRegister(lastResourceVersion)
reflectormetrics.SetReflectorMetricsProvider(reflectorMetricsProvider{})
}
// this section contains adapters, implementations, and other sundry organic, artisanally
// hand-crafted syntax trees required to convince client-go that it actually wants to let
// someone use its metrics.
@@ -191,41 +109,3 @@ type resultAdapter struct {
func (r *resultAdapter) Increment(_ context.Context, code, method, host string) {
r.metric.WithLabelValues(code, method, host).Inc()
}
// Reflector metrics provider (method #2 for client-go metrics),
// copied (more-or-less directly) from k8s.io/kubernetes setup code
// (which isn't anywhere in an easily-importable place).
type reflectorMetricsProvider struct{}
func (reflectorMetricsProvider) NewListsMetric(name string) reflectormetrics.CounterMetric {
return listsTotal.WithLabelValues(name)
}
func (reflectorMetricsProvider) NewListDurationMetric(name string) reflectormetrics.SummaryMetric {
return listsDuration.WithLabelValues(name)
}
func (reflectorMetricsProvider) NewItemsInListMetric(name string) reflectormetrics.SummaryMetric {
return itemsPerList.WithLabelValues(name)
}
func (reflectorMetricsProvider) NewWatchesMetric(name string) reflectormetrics.CounterMetric {
return watchesTotal.WithLabelValues(name)
}
func (reflectorMetricsProvider) NewShortWatchesMetric(name string) reflectormetrics.CounterMetric {
return shortWatchesTotal.WithLabelValues(name)
}
func (reflectorMetricsProvider) NewWatchDurationMetric(name string) reflectormetrics.SummaryMetric {
return watchDuration.WithLabelValues(name)
}
func (reflectorMetricsProvider) NewItemsInWatchMetric(name string) reflectormetrics.SummaryMetric {
return itemsPerWatch.WithLabelValues(name)
}
func (reflectorMetricsProvider) NewLastResourceVersionMetric(name string) reflectormetrics.GaugeMetric {
return lastResourceVersion.WithLabelValues(name)
}

View File

@@ -0,0 +1,40 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"k8s.io/client-go/tools/leaderelection"
)
// This file is copied and adapted from k8s.io/component-base/metrics/prometheus/clientgo/leaderelection
// which registers metrics to the k8s legacy Registry. We require very
// similar functionality, but must register metrics to a different Registry.
var (
leaderGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "leader_election_master_status",
Help: "Gauge of if the reporting system is master of the relevant lease, 0 indicates backup, 1 indicates master. 'name' is the string used to identify the lease. Please make sure to group by name.",
}, []string{"name"})
)
func init() {
Registry.MustRegister(leaderGauge)
leaderelection.SetProvider(leaderelectionMetricsProvider{})
}
type leaderelectionMetricsProvider struct{}
func (leaderelectionMetricsProvider) NewLeaderMetric() leaderelection.SwitchMetric {
return &switchAdapter{gauge: leaderGauge}
}
type switchAdapter struct {
gauge *prometheus.GaugeVec
}
func (s *switchAdapter) On(name string) {
s.gauge.WithLabelValues(name).Set(1.0)
}
func (s *switchAdapter) Off(name string) {
s.gauge.WithLabelValues(name).Set(0.0)
}

View File

@@ -21,8 +21,8 @@ import (
"k8s.io/client-go/util/workqueue"
)
// This file is copied and adapted from k8s.io/kubernetes/pkg/util/workqueue/prometheus
// which registers metrics to the default prometheus Registry. We require very
// This file is copied and adapted from k8s.io/component-base/metrics/prometheus/workqueue
// which registers metrics to the k8s legacy Registry. We require very
// similar functionality, but must register metrics to a different Registry.
// Metrics subsystem and all keys used by the workqueue.

View File

@@ -50,6 +50,7 @@ var _ Predicate = GenerationChangedPredicate{}
var _ Predicate = AnnotationChangedPredicate{}
var _ Predicate = or{}
var _ Predicate = and{}
var _ Predicate = not{}
// Funcs is a function that implements Predicate.
type Funcs struct {
@@ -176,9 +177,9 @@ func (GenerationChangedPredicate) Update(e event.UpdateEvent) bool {
// This predicate will skip update events that have no change in the object's annotation.
// It is intended to be used in conjunction with the GenerationChangedPredicate, as in the following example:
//
// Controller.Watch(
// Controller.Watch(
// &source.Kind{Type: v1.MyCustomKind},
// &handler.EnqueueRequestForObject{},
// &handler.EnqueueRequestForObject{},
// predicate.Or(predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{}))
//
// This is mostly useful for controllers that needs to trigger both when the resource's generation is incremented
@@ -207,9 +208,10 @@ func (AnnotationChangedPredicate) Update(e event.UpdateEvent) bool {
// It is intended to be used in conjunction with the GenerationChangedPredicate, as in the following example:
//
// Controller.Watch(
// &source.Kind{Type: v1.MyCustomKind},
// &handler.EnqueueRequestForObject{},
// predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{}))
//
// &source.Kind{Type: v1.MyCustomKind},
// &handler.EnqueueRequestForObject{},
// predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{}))
//
// This will be helpful when object's labels is carrying some extra specification information beyond object's spec,
// and the controller will be triggered if any valid spec change (not only in spec, but also in labels) happens.
@@ -339,6 +341,35 @@ func (o or) Generic(e event.GenericEvent) bool {
return false
}
// Not returns a predicate that implements a logical NOT of the predicate passed to it.
func Not(predicate Predicate) Predicate {
return not{predicate}
}
type not struct {
predicate Predicate
}
func (n not) InjectFunc(f inject.Func) error {
return f(n.predicate)
}
func (n not) Create(e event.CreateEvent) bool {
return !n.predicate.Create(e)
}
func (n not) Update(e event.UpdateEvent) bool {
return !n.predicate.Update(e)
}
func (n not) Delete(e event.DeleteEvent) bool {
return !n.predicate.Delete(e)
}
func (n not) Generic(e event.GenericEvent) bool {
return !n.predicate.Generic(e)
}
// LabelSelectorPredicate constructs a Predicate from a LabelSelector.
// Only objects matching the LabelSelector will be admitted.
func LabelSelectorPredicate(s metav1.LabelSelector) (Predicate, error) {

View File

@@ -61,24 +61,24 @@ Deleting Kubernetes objects) or external Events (GitHub Webhooks, polling extern
Example reconcile Logic:
* Read an object and all the Pods it owns.
* Observe that the object spec specifies 5 replicas but actual cluster contains only 1 Pod replica.
* Create 4 Pods and set their OwnerReferences to the object.
* Read an object and all the Pods it owns.
* Observe that the object spec specifies 5 replicas but actual cluster contains only 1 Pod replica.
* Create 4 Pods and set their OwnerReferences to the object.
reconcile may be implemented as either a type:
type reconcile struct {}
type reconciler struct {}
func (reconcile) reconcile(controller.Request) (controller.Result, error) {
func (reconciler) Reconcile(ctx context.Context, o reconcile.Request) (reconcile.Result, error) {
// Implement business logic of reading and writing objects here
return controller.Result{}, nil
return reconcile.Result{}, nil
}
Or as a function:
controller.Func(func(o controller.Request) (controller.Result, error) {
reconcile.Func(func(ctx context.Context, o reconcile.Request) (reconcile.Result, error) {
// Implement business logic of reading and writing objects here
return controller.Result{}, nil
return reconcile.Result{}, nil
})
Reconciliation is level-based, meaning action isn't driven off changes in individual Events, but instead is

View File

@@ -21,37 +21,36 @@ limitations under the License.
// Each API group should define a utility function
// called AddToScheme for adding its types to a Scheme:
//
// // in package myapigroupv1...
// var (
// SchemeGroupVersion = schema.GroupVersion{Group: "my.api.group", Version: "v1"}
// SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
// AddToScheme = SchemeBuilder.AddToScheme
// )
// // in package myapigroupv1...
// var (
// SchemeGroupVersion = schema.GroupVersion{Group: "my.api.group", Version: "v1"}
// SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
// AddToScheme = SchemeBuilder.AddToScheme
// )
//
// func init() {
// SchemeBuilder.Register(&MyType{}, &MyTypeList)
// }
// var (
// scheme *runtime.Scheme = runtime.NewScheme()
// )
// func init() {
// SchemeBuilder.Register(&MyType{}, &MyTypeList)
// }
// var (
// scheme *runtime.Scheme = runtime.NewScheme()
// )
//
// This also true of the built-in Kubernetes types. Then, in the entrypoint for
// your manager, assemble the scheme containing exactly the types you need,
// panicing if scheme registration failed. For instance, if our controller needs
// types from the core/v1 API group (e.g. Pod), plus types from my.api.group/v1:
//
// func init() {
// utilruntime.Must(myapigroupv1.AddToScheme(scheme))
// utilruntime.Must(kubernetesscheme.AddToScheme(scheme))
// }
//
// func main() {
// mgr := controllers.NewManager(context.Background(), controllers.GetConfigOrDie(), manager.Options{
// Scheme: scheme,
// })
// // ...
// }
// func init() {
// utilruntime.Must(myapigroupv1.AddToScheme(scheme))
// utilruntime.Must(kubernetesscheme.AddToScheme(scheme))
// }
//
// func main() {
// mgr := controllers.NewManager(context.Background(), controllers.GetConfigOrDie(), manager.Options{
// Scheme: scheme,
// })
// // ...
// }
package scheme
import (

View File

@@ -83,6 +83,10 @@ func (ks *kindWithCache) Start(ctx context.Context, handler handler.EventHandler
return ks.kind.Start(ctx, handler, queue, prct...)
}
func (ks *kindWithCache) String() string {
return ks.kind.String()
}
func (ks *kindWithCache) WaitForSync(ctx context.Context) error {
return ks.kind.WaitForSync(ctx)
}
@@ -155,7 +159,11 @@ func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue w
return
}
i.AddEventHandler(internal.EventHandler{Queue: queue, EventHandler: handler, Predicates: prct})
_, err := i.AddEventHandler(internal.EventHandler{Queue: queue, EventHandler: handler, Predicates: prct})
if err != nil {
ks.started <- err
return
}
if !ks.cache.WaitForCacheSync(ctx) {
// Would be great to return something more informative here
ks.started <- errors.New("cache did not sync")
@@ -351,7 +359,10 @@ func (is *Informer) Start(ctx context.Context, handler handler.EventHandler, que
return fmt.Errorf("must specify Informer.Informer")
}
is.Informer.AddEventHandler(internal.EventHandler{Queue: queue, EventHandler: handler, Predicates: prct})
_, err := is.Informer.AddEventHandler(internal.EventHandler{Queue: queue, EventHandler: handler, Predicates: prct})
if err != nil {
return err
}
return nil
}

View File

@@ -21,6 +21,8 @@ import (
"encoding/json"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -56,6 +58,17 @@ func (h *mutatingHandler) Handle(ctx context.Context, req Request) Response {
panic("defaulter should never be nil")
}
// always skip when a DELETE operation received in mutation handler
// describe in https://github.com/kubernetes-sigs/controller-runtime/issues/1762
if req.Operation == admissionv1.Delete {
return Response{AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Code: http.StatusOK,
},
}}
}
// Get the object in the request
obj := h.defaulter.DeepCopyObject().(Defaulter)
if err := h.decoder.Decode(req, obj); err != nil {

View File

@@ -19,11 +19,12 @@ package admission
import (
"context"
"encoding/json"
"errors"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -61,6 +62,18 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
panic("object should never be nil")
}
// Always skip when a DELETE operation received in custom mutation handler.
if req.Operation == admissionv1.Delete {
return Response{AdmissionResponse: admissionv1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Code: http.StatusOK,
},
}}
}
ctx = NewContextWithRequest(ctx, req)
// Get the object in the request
obj := h.object.DeepCopyObject()
if err := h.decoder.Decode(req, obj); err != nil {

View File

@@ -124,8 +124,16 @@ func (wh *Webhook) writeResponseTyped(w io.Writer, response Response, admRevGVK
// writeAdmissionResponse writes ar to w.
func (wh *Webhook) writeAdmissionResponse(w io.Writer, ar v1.AdmissionReview) {
if err := json.NewEncoder(w).Encode(ar); err != nil {
wh.log.Error(err, "unable to encode the response")
wh.writeResponse(w, Errored(http.StatusInternalServerError, err))
wh.log.Error(err, "unable to encode and write the response")
// Since the `ar v1.AdmissionReview` is a clear and legal object,
// it should not have problem to be marshalled into bytes.
// The error here is probably caused by the abnormal HTTP connection,
// e.g., broken pipe, so we can only write the error response once,
// to avoid endless circular calling.
serverError := Errored(http.StatusInternalServerError, err)
if err = json.NewEncoder(w).Encode(v1.AdmissionReview{Response: &serverError.AdmissionResponse}); err != nil {
wh.log.Error(err, "still unable to encode and write the InternalServerError response")
}
} else {
res := ar.Response
if log := wh.log; log.V(1).Enabled() {

View File

@@ -64,6 +64,8 @@ func (h *validatorForType) Handle(ctx context.Context, req Request) Response {
panic("object should never be nil")
}
ctx = NewContextWithRequest(ctx, req)
// Get the object in the request
obj := h.object.DeepCopyObject()

View File

@@ -19,6 +19,7 @@ package admission
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/go-logr/logr"
@@ -27,6 +28,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes/scheme"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
@@ -121,6 +123,9 @@ type Webhook struct {
// and potentially patches to apply to the handler.
Handler Handler
// RecoverPanic indicates whether the panic caused by webhook should be recovered.
RecoverPanic bool
// WithContextFunc will allow you to take the http.Request.Context() and
// add any additional information such as passing the request path or
// headers thus allowing you to read them from within the handler
@@ -138,11 +143,29 @@ func (wh *Webhook) InjectLogger(l logr.Logger) error {
return nil
}
// WithRecoverPanic takes a bool flag which indicates whether the panic caused by webhook should be recovered.
func (wh *Webhook) WithRecoverPanic(recoverPanic bool) *Webhook {
wh.RecoverPanic = recoverPanic
return wh
}
// Handle processes AdmissionRequest.
// If the webhook is mutating type, it delegates the AdmissionRequest to each handler and merge the patches.
// If the webhook is validating type, it delegates the AdmissionRequest to each handler and
// deny the request if anyone denies.
func (wh *Webhook) Handle(ctx context.Context, req Request) Response {
func (wh *Webhook) Handle(ctx context.Context, req Request) (response Response) {
if wh.RecoverPanic {
defer func() {
if r := recover(); r != nil {
for _, fn := range utilruntime.PanicHandlers {
fn(r)
}
response = Errored(http.StatusInternalServerError, fmt.Errorf("panic: %v [recovered]", r))
return
}
}()
}
resp := wh.Handler.Handle(ctx, req)
if err := resp.Complete(req); err != nil {
wh.log.Error(err, "unable to encode response")
@@ -253,3 +276,21 @@ func StandaloneWebhook(hook *Webhook, opts StandaloneOptions) (http.Handler, err
}
return metrics.InstrumentedHook(opts.MetricsPath, hook), nil
}
// requestContextKey is how we find the admission.Request in a context.Context.
type requestContextKey struct{}
// RequestFromContext returns an admission.Request from ctx.
func RequestFromContext(ctx context.Context) (Request, error) {
if v, ok := ctx.Value(requestContextKey{}).(Request); ok {
return v, nil
}
return Request{}, errors.New("admission.Request not found in context")
}
// NewContextWithRequest returns a new Context, derived from ctx, which carries the
// provided admission.Request.
func NewContextWithRequest(ctx context.Context, req Request) context.Context {
return context.WithValue(ctx, requestContextKey{}, req)
}

View File

@@ -69,6 +69,12 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if convertReview.Request == nil {
log.Error(nil, "conversion request is nil")
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO(droot): may be move the conversion logic to a separate module to
// decouple it from the http layer ?
resp, err := wh.handleConvertRequest(convertReview.Request)

View File

@@ -74,8 +74,12 @@ type Server struct {
// TLSVersion is the minimum version of TLS supported. Accepts
// "", "1.0", "1.1", "1.2" and "1.3" only ("" is equivalent to "1.0" for backwards compatibility)
// Deprecated: Use TLSOpts instead.
TLSMinVersion string
// TLSOpts is used to allow configuring the TLS config used for the server
TLSOpts []func(*tls.Config)
// WebhookMux is the multiplexer that handles different webhooks.
WebhookMux *http.ServeMux
@@ -254,6 +258,11 @@ func (s *Server) Start(ctx context.Context) error {
cfg.ClientAuth = tls.RequireAndVerifyClientCert
}
// fallback TLS config ready, will now mutate if passer wants full control over it
for _, op := range s.TLSOpts {
op(cfg)
}
listener, err := tls.Listen("tcp", net.JoinHostPort(s.Host, strconv.Itoa(s.Port)), cfg)
if err != nil {
return err
@@ -291,7 +300,7 @@ func (s *Server) Start(ctx context.Context) error {
// server has been started.
func (s *Server) StartedChecker() healthz.Checker {
config := &tls.Config{
InsecureSkipVerify: true, // nolint:gosec // config is used to connect to our own webhook port.
InsecureSkipVerify: true, //nolint:gosec // config is used to connect to our own webhook port.
}
return func(req *http.Request) error {
s.mu.Lock()

View File

@@ -24,6 +24,8 @@ const (
ClusterReady ClusterConditionType = "Ready"
// ClusterOffline means the cluster is temporarily down or not reachable
ClusterOffline ClusterConditionType = "Offline"
// ClusterConfigMalformed means the cluster's configuration may be malformed.
ClusterConfigMalformed ClusterConditionType = "ConfigMalformed"
)
const (

View File

@@ -70,6 +70,9 @@ type LocalSecretReference struct {
type KubeFedClusterStatus struct {
// Conditions is an array of current cluster conditions.
Conditions []ClusterCondition `json:"conditions"`
// KubernetesVersion is the Kubernetes git version of the cluster.
// +optional
KubernetesVersion string `json:"kubernetesVersion,omitempty"`
// Zones are the names of availability zones in which the nodes of the cluster exist, e.g. 'us-east1-a'.
// +optional
Zones []string `json:"zones,omitempty"`
@@ -81,7 +84,8 @@ type KubeFedClusterStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name=age,type=date,JSONPath=.metadata.creationTimestamp
// +kubebuilder:printcolumn:name=ready,type=string,JSONPath=.status.conditions[?(@.type=='Ready')].status
// +kubebuilder:resource:path=kubefedclusters
// +kubebuilder:printcolumn:name=kubernetes-version,type=string,JSONPath=.status.kubernetesVersion
// +kubebuilder:resource:path=kubefedclusters,shortName=kfc
// +kubebuilder:subresource:status
// KubeFedCluster configures KubeFed to be aware of a Kubernetes

View File

@@ -48,6 +48,9 @@ type DurationConfig struct {
// Time to wait before giving up on an unhealthy cluster.
// +optional
UnavailableDelay *metav1.Duration `json:"unavailableDelay,omitempty"`
// Time to wait for all caches to sync before exit.
// +optional
CacheSyncTimeout *metav1.Duration `json:"cacheSyncTimeout,omitempty"`
}
type LeaderElectConfig struct {
// The duration that non-leader candidates will wait after observing a leadership

View File

@@ -118,6 +118,11 @@ func (in *DurationConfig) DeepCopyInto(out *DurationConfig) {
*out = new(v1.Duration)
**out = **in
}
if in.CacheSyncTimeout != nil {
in, out := &in.CacheSyncTimeout, &out.CacheSyncTimeout
*out = new(v1.Duration)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DurationConfig.

View File

@@ -124,26 +124,31 @@ func CustomizeTLSTransport(fedCluster *fedv1b1.KubeFedCluster, clientConfig *res
return errors.Errorf("Cluster %s transport error: %s", fedCluster.Name, err)
}
err = CustomizeCertificateValidation(fedCluster, transportConfig)
if err != nil {
return errors.Errorf("Cluster %s custom certificate validation error: %s", fedCluster.Name, err)
if transportConfig != nil {
err = CustomizeCertificateValidation(fedCluster, transportConfig)
if err != nil {
return errors.Errorf("Cluster %s custom certificate validation error: %s", fedCluster.Name, err)
}
// using the same defaults as http.DefaultTransport
clientConfig.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: transportConfig,
}
clientConfig.TLSClientConfig = restclient.TLSClientConfig{}
} else {
clientConfig.Insecure = true
}
// using the same defaults as http.DefaultTransport
clientConfig.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: transportConfig,
}
clientConfig.TLSClientConfig = restclient.TLSClientConfig{}
return nil
}

View File

@@ -20,9 +20,11 @@ import (
"time"
)
// Providing 0 duration to an informer indicates that resync should be delayed as long as possible
const (
NoResyncPeriod time.Duration = 0 * time.Second
// Providing 0 duration to an informer indicates that resync should be delayed as long as possible
NoResyncPeriod = 0 * time.Second
SyncedPollPeriod = 10 * time.Second
NamespaceName = "namespaces"
NamespaceKind = "Namespace"
@@ -39,6 +41,12 @@ const (
StatusField = "status"
MetadataField = "metadata"
// Service fields
HealthCheckNodePortField = "healthCheckNodePort"
ClusterIPField = "clusterIP"
ClusterIPsField = "clusterIPs"
PortsField = "ports"
// ServiceAccount fields
SecretsField = "secrets"

View File

@@ -72,6 +72,7 @@ type ControllerConfig struct {
ClusterAvailableDelay time.Duration
ClusterUnavailableDelay time.Duration
MinimizeLatency bool
CacheSyncTimeout time.Duration
MaxConcurrentSyncReconciles int64
MaxConcurrentStatusReconciles int64
SkipAdoptingResources bool

View File

@@ -534,33 +534,24 @@ func (fs *federatedStoreImpl) GetKeyFor(item interface{}) string {
return key
}
// Checks whether stores for all clusters form the lists (and only these) are there and
// ClustersSynced checks whether stores for all clusters form the lists (and only these) are there and
// are synced.
func (fs *federatedStoreImpl) ClustersSynced(clusters []*fedv1b1.KubeFedCluster) bool {
// Get the list of informers to check under a lock and check it outside.
okSoFar, informersToCheck := func() (bool, []informer) {
fs.federatedInformer.Lock()
defer fs.federatedInformer.Unlock()
fs.federatedInformer.Lock()
defer fs.federatedInformer.Unlock()
if len(fs.federatedInformer.targetInformers) != len(clusters) {
return false, []informer{}
}
informersToCheck := make([]informer, 0, len(clusters))
for _, cluster := range clusters {
if targetInformer, found := fs.federatedInformer.targetInformers[cluster.Name]; found {
informersToCheck = append(informersToCheck, targetInformer)
} else {
return false, []informer{}
}
}
return true, informersToCheck
}()
if !okSoFar {
if len(fs.federatedInformer.targetInformers) != len(clusters) {
klog.V(4).Infof("The number of target informers mismatch with given clusters")
return false
}
for _, informerToCheck := range informersToCheck {
if !informerToCheck.controller.HasSynced() {
for _, cluster := range clusters {
if targetInformer, found := fs.federatedInformer.targetInformers[cluster.Name]; found {
if !targetInformer.controller.HasSynced() {
klog.V(4).Infof("Informer of cluster %q not synced", cluster.Name)
return false
}
} else {
klog.V(4).Infof("Informer of cluster %q not found", cluster.Name)
return false
}
}

View File

@@ -23,6 +23,8 @@ import (
"k8s.io/client-go/util/flowcontrol"
"k8s.io/client-go/util/workqueue"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kubefed/pkg/metrics"
)
type ReconcileFunc func(qualifiedName QualifiedName) ReconciliationStatus
@@ -128,6 +130,8 @@ func (w *asyncWorker) EnqueueWithDelay(qualifiedName QualifiedName, delay time.D
}
func (w *asyncWorker) Run(stopChan <-chan struct{}) {
w.initMetrics()
StartBackoffGC(w.backoff, stopChan)
w.deliverer.StartWithHandler(func(item *DelayingDelivererItem) {
qualifiedName, ok := item.Value.(*QualifiedName)
@@ -183,16 +187,41 @@ func (w *asyncWorker) reconcileOnce() bool {
return true
}
metrics.ControllerRuntimeActiveWorkers.WithLabelValues(w.name).Add(1)
defer metrics.ControllerRuntimeActiveWorkers.WithLabelValues(w.name).Add(-1)
defer metrics.UpdateControllerRuntimeReconcileTimeFromStart(w.name, time.Now())
status := w.reconcile(qualifiedName)
switch status {
case StatusAllOK:
break
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelSuccess).Inc()
case StatusError:
w.EnqueueForError(qualifiedName)
metrics.ControllerRuntimeReconcileErrors.WithLabelValues(w.name).Inc()
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelError).Inc()
case StatusNeedsRecheck:
w.EnqueueForRetry(qualifiedName)
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelNeedsRecheck).Inc()
case StatusNotSynced:
w.EnqueueForClusterSync(qualifiedName)
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelNotSynced).Inc()
}
return true
}
const (
labelSuccess = "success"
labelError = "error"
labelNeedsRecheck = "needs_recheck"
labelNotSynced = "not_synced"
)
func (w *asyncWorker) initMetrics() {
metrics.ControllerRuntimeActiveWorkers.WithLabelValues(w.name).Set(0)
metrics.ControllerRuntimeReconcileErrors.WithLabelValues(w.name).Add(0)
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelSuccess).Add(0)
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelError).Add(0)
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelNeedsRecheck).Add(0)
metrics.ControllerRuntimeReconcileTotal.WithLabelValues(w.name, labelNotSynced).Add(0)
metrics.ControllerRuntimeWorkerCount.WithLabelValues(w.name).Set(float64(w.maxConcurrentReconciles))
}

View File

@@ -107,6 +107,12 @@ func ClusterServiceAccountName(joiningClusterName, hostClusterName string) strin
return fmt.Sprintf("%s-%s", joiningClusterName, hostClusterName)
}
// ClusterServiceAccountTokenSecretName returns the name of a service account token secret whose
// credentials are used by the host cluster to access the client cluster.
func ClusterServiceAccountTokenSecretName(joiningClusterName, hostClusterName string) string {
return fmt.Sprintf("%s-%s", joiningClusterName, hostClusterName)
}
// RoleName returns the name of a Role or ClusterRole and its
// associated RoleBinding or ClusterRoleBinding that are used to allow
// the service account to access necessary resources on the cluster.

View File

@@ -20,6 +20,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
@@ -58,7 +59,7 @@ var (
reconcileFederatedResourcesDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "reconcile_federated_resources_duration_seconds",
Help: "Time taken to reconcile federated resources in the target clusters.",
Help: "[Deprecated] Time taken to reconcile federated resources in the target clusters. Replaced by controller_runtime_reconcile_time_seconds.",
Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5, 20.0, 22.5, 25.0, 27.5, 30.0, 50.0, 75.0, 100.0, 1000.0},
},
)
@@ -90,7 +91,7 @@ var (
controllerRuntimeReconcileDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "controller_runtime_reconcile_duration_seconds",
Help: "Time taken by various parts of Kubefed controllers reconciliation loops.",
Help: "[Deprecated] Time taken by various parts of Kubefed controllers reconciliation loops. Replaced by controller_runtime_reconcile_time_seconds.",
Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5, 20.0, 22.5, 25.0, 27.5, 30.0, 50.0, 75.0, 100.0, 1000.0},
}, []string{"controller"},
)
@@ -98,10 +99,37 @@ var (
controllerRuntimeReconcileDurationSummary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "controller_runtime_reconcile_quantile_seconds",
Help: "Quantiles of time taken by various parts of Kubefed controllers reconciliation loops.",
Help: "[Deprecated] Quantiles of time taken by various parts of Kubefed controllers reconciliation loops. Replaced by controller_runtime_reconcile_time_seconds.",
MaxAge: time.Hour,
}, []string{"controller"},
)
ControllerRuntimeReconcileTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "controller_runtime_reconcile_total",
Help: "Total number of reconciliations per controller",
}, []string{"controller", "result"})
ControllerRuntimeReconcileErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "controller_runtime_reconcile_errors_total",
Help: "Total number of reconciliation errors per controller",
}, []string{"controller"})
ControllerRuntimeReconcileTime = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "controller_runtime_reconcile_time_seconds",
Help: "Length of time per reconciliation per controller",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 50, 60},
}, []string{"controller"})
ControllerRuntimeWorkerCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "controller_runtime_max_concurrent_reconciles",
Help: "Maximum number of concurrent reconciles per controller",
}, []string{"controller"})
ControllerRuntimeActiveWorkers = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "controller_runtime_active_workers",
Help: "Number of currently used workers per controller",
}, []string{"controller"})
)
const (
@@ -117,6 +145,10 @@ const (
// RegisterAll registers all metrics.
func RegisterAll() {
metrics.Registry.MustRegister(
// expose process metrics like CPU, Memory, file descriptor usage etc.
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
// expose Go runtime metrics like GC stats, memory stats etc.
collectors.NewGoCollector(),
kubefedClusterTotal,
joinedClusterTotal,
reconcileFederatedResourcesDuration,
@@ -127,6 +159,11 @@ func RegisterAll() {
dispatchOperationDuration,
controllerRuntimeReconcileDuration,
controllerRuntimeReconcileDurationSummary,
ControllerRuntimeReconcileTotal,
ControllerRuntimeReconcileErrors,
ControllerRuntimeReconcileTime,
ControllerRuntimeWorkerCount,
ControllerRuntimeActiveWorkers,
)
}
@@ -203,10 +240,20 @@ func UpdateControllerReconcileDurationFromStart(controller string, start time.Ti
// UpdateControllerReconcileDuration records the duration of the reconcile function of a controller
func UpdateControllerReconcileDuration(controller string, duration time.Duration) {
if duration > LogReconcileLongDurationThreshold {
klog.V(4).Infof("Reconcile loop %s took %v to complete", controller, duration)
}
controllerRuntimeReconcileDurationSummary.WithLabelValues(controller).Observe(duration.Seconds())
controllerRuntimeReconcileDuration.WithLabelValues(controller).Observe(duration.Seconds())
}
// UpdateControllerRuntimeReconcileTimeFromStart records the duration of the reconcile loop of a controller
func UpdateControllerRuntimeReconcileTimeFromStart(controller string, start time.Time) {
duration := time.Since(start)
UpdateControllerRuntimeReconcileTime(controller, duration)
}
// UpdateControllerRuntimeReconcileTime records the duration of the reconcile function of a controller
func UpdateControllerRuntimeReconcileTime(controller string, duration time.Duration) {
if duration > LogReconcileLongDurationThreshold {
klog.V(4).Infof("Reconcile loop %s took %v to complete", controller, duration)
}
ControllerRuntimeReconcileTime.WithLabelValues(controller).Observe(duration.Seconds())
}