add controler runtime dep

This commit is contained in:
Michael Henriksen 2018-11-30 07:50:38 -05:00
parent d675f2fe76
commit 8dc6469e1f
197 changed files with 25784 additions and 2 deletions

6
glide.lock generated
View File

@ -1,5 +1,5 @@
hash: ba95291a0e4b936a6cf436071416090ef19cfbac8b44b4253792e28d305252ae
updated: 2019-01-08T19:15:38.184275352Z
hash: 65be6b54091a3194f1d3d60aef81dbf98945f380e58318e661deee0a3880ef88
updated: 2019-01-08T20:51:55.484318811Z
imports:
- name: github.com/beorn7/perks
version: 3ac7bf7a47d159a033b107610db8a1b6575507a4
@ -514,4 +514,6 @@ imports:
subpackages:
- pkg/ginkgo-reporters
- pkg/polarion-xml
- name: sigs.k8s.io/controller-runtime
version: 5fd1e9e9fac5261e9ad9d47c375afc014fc31d21
testImports: []

View File

@ -28,3 +28,5 @@ import:
version: 0.9.2
- package: github.com/emicklei/go-restful-openapi
version: ^1.0.0
- package: sigs.k8s.io/controller-runtime
version: v0.1.7

18
vendor/sigs.k8s.io/controller-runtime/.gitignore generated vendored Normal file
View File

@ -0,0 +1,18 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~

26
vendor/sigs.k8s.io/controller-runtime/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,26 @@
language: go
os:
- linux
- osx
go:
- "1.10"
git:
depth: 3
go_import_path: sigs.k8s.io/controller-runtime
install:
- go get -u github.com/golang/dep/cmd/dep
#- go get -u golang.org/x/lint/golint
- go get -u gopkg.in/alecthomas/gometalinter.v2 && gometalinter.v2 --install
script:
- TRACE=1 ./hack/check-everything.sh
# TBD. Suppressing for now.
notifications:
email: false

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

@ -0,0 +1,29 @@
# Contributing guidelines
## Sign the CLA
Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests.
Please see https://git.k8s.io/community/CLA.md for more info
## Contributing steps
1. Submit an issue describing your proposed change to the repo in question.
1. The [repo owners](OWNERS) will respond to your issue promptly.
1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above).
1. Fork the desired repo, develop and test your code changes.
1. Submit a pull request.
## Test locally
1. Setup tools
```bash
$ go get -u github.com/golang/dep/cmd/dep
$ go get -u gopkg.in/alecthomas/gometalinter.v2
$ gometalinter.v2 --install # if can't load package, refer: https://github.com/alecthomas/gometalinter/issues/404
```
1. Test
```bash
TRACE=1 ./hack/check-everything.sh
```

1375
vendor/sigs.k8s.io/controller-runtime/Gopkg.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

77
vendor/sigs.k8s.io/controller-runtime/Gopkg.toml generated vendored Normal file
View File

@ -0,0 +1,77 @@
# Copyright 2018 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.
# Packages required by users
required = ["sigs.k8s.io/testing_frameworks/integration",
"k8s.io/client-go/plugin/pkg/client/auth",
"github.com/spf13/pflag",
"github.com/emicklei/go-restful",
"github.com/go-openapi/spec",
"k8s.io/kube-openapi/pkg/common",
"k8s.io/apiextensions-apiserver",
]
[[constraint]]
name = "k8s.io/api"
version = "kubernetes-1.11.2"
[[constraint]]
name = "k8s.io/apiextensions-apiserver"
version = "kubernetes-1.11.2"
[[constraint]]
name = "k8s.io/apimachinery"
version = "kubernetes-1.11.2"
[[constraint]]
name = "k8s.io/client-go"
version = "kubernetes-1.11.2"
[[constraint]]
name = "github.com/onsi/ginkgo"
version = "v1.5.0"
[[constraint]]
name = "github.com/onsi/gomega"
version = "v1.4.0"
[[constraint]]
name = "github.com/ghodss/yaml"
version = "1.0.0"
[[constraint]]
name = "go.uber.org/zap"
version = "1.8.0"
# these are not listed explicitly until we get version tags,
# since dep doesn't like bare revision dependencies
# [[constraint]]
# name = "sigs.k8s.io/testing_frameworks"
#
# [[constraint]]
# name = "github.com/go-logr/logr"
#
# [[constraint]]
# name = "github.com/go-logr/zapr"
# For dependency below: Refer to issue https://github.com/golang/dep/issues/1799
[[override]]
name = "gopkg.in/fsnotify.v1"
source = "https://github.com/fsnotify/fsnotify.git"
version="v1.4.7"
[prune]
go-tests = true
unused-packages = true

201
vendor/sigs.k8s.io/controller-runtime/LICENSE generated vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

11
vendor/sigs.k8s.io/controller-runtime/OWNERS generated vendored Normal file
View File

@ -0,0 +1,11 @@
# See the OWNERS docs: https://git.k8s.io/community/contributors/devel/owners.md
owners:
- directxman12
- droot
- pwittrock
approvers:
- controller-runtime-admins
- controller-runtime-maintainers
reviewers:
- controller-runtime-admins
- controller-runtime-maintainers

8
vendor/sigs.k8s.io/controller-runtime/OWNERS_ALIASES generated vendored Normal file
View File

@ -0,0 +1,8 @@
# See the OWNERS docs: https://git.k8s.io/community/contributors/devel/owners.md
aliases:
controller-runtime-admins:
- directxman12
- droot
- pwittrock
controller-runtime-maintainers: []

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

@ -0,0 +1,36 @@
[![Build Status](https://travis-ci.org/kubernetes-sigs/controller-runtime.svg?branch=master)](https://travis-ci.org/kubernetes-sigs/controller-runtime "Travis")
[![Go Report Card](https://goreportcard.com/badge/sigs.k8s.io/controller-runtime)](https://goreportcard.com/report/sigs.k8s.io/controller-runtime)
# Kubernetes controller-runtime Project
The Kubernetes controller-runtime Project is a set of go libraries for building Controllers.
Documentation:
- [Package overview](https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg)
- [Basic controller using builder](https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/builder#example-Builder)
- [Creating a manager](https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/manager#example-New)
- [Creating a controller](https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/controller#example-New)
- [Example `main.go`](https://github.com/kubernetes-sigs/controller-runtime/blob/master/example/main.go)
## Community, discussion, contribution, and support
Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/).
controller-runtime is a subproject of the [kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) project
in sig apimachinery.
You can reach the maintainers of this project at:
- Slack channel: [#kubebuilder](http://slack.k8s.io/#kubebuilder)
- Google Group: [kubebuilder@googlegroups.com](https://groups.google.com/forum/#!forum/kubebuilder)
## Contributing
Contributions are greatly appreciated. The maintainers actively manage the issues list, and try to highlight issues suitable for newcomers.
The project follows the typical GitHub pull request model. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
Before starting any work, please either comment on an existing issue, or file a new one.
## Code of conduct
Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md).

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

@ -0,0 +1,8 @@
# Release Process
The Kubernetes controller-runtime Project is released on an as-needed basis. The process is as follows:
1. An issue is proposing a new release with a changelog since the last release
1. 2 [OWNERS](OWNERS) must LGTM this release
1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION`
1. The release issue is closed

View File

@ -0,0 +1,15 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Team to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
directxman12
pwittrock
droot

View File

@ -0,0 +1,3 @@
# Kubernetes Community Code of Conduct
Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)

View File

@ -0,0 +1,77 @@
/*
Copyright 2018 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 main
import (
"context"
"github.com/go-logr/logr"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// reconcileReplicaSet reconciles ReplicaSets
type reconcileReplicaSet struct {
// client can be used to retrieve objects from the APIServer.
client client.Client
log logr.Logger
}
// Implement reconcile.Reconciler so the controller can reconcile objects
var _ reconcile.Reconciler = &reconcileReplicaSet{}
func (r *reconcileReplicaSet) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// set up a convinient log object so we don't have to type request over and over again
log := r.log.WithValues("request", request)
// Fetch the ReplicaSet from the cache
rs := &appsv1.ReplicaSet{}
err := r.client.Get(context.TODO(), request.NamespacedName, rs)
if errors.IsNotFound(err) {
log.Error(nil, "Could not find ReplicaSet")
return reconcile.Result{}, nil
}
if err != nil {
log.Error(err, "Could not fetch ReplicaSet")
return reconcile.Result{}, err
}
// Print the ReplicaSet
log.Info("Reconciling ReplicaSet", "container name", rs.Spec.Template.Spec.Containers[0].Name)
// Set the label if it is missing
if rs.Labels == nil {
rs.Labels = map[string]string{}
}
if rs.Labels["hello"] == "world" {
return reconcile.Result{}, nil
}
// Update the ReplicaSet
rs.Labels["hello"] = "world"
err = r.client.Update(context.TODO(), rs)
if err != nil {
log.Error(err, "Could not write ReplicaSet")
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}

147
vendor/sigs.k8s.io/controller-runtime/example/main.go generated vendored Normal file
View File

@ -0,0 +1,147 @@
/*
Copyright 2018 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 main
import (
"flag"
"os"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apitypes "k8s.io/apimachinery/pkg/types"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/builder"
)
var log = logf.Log.WithName("example-controller")
func main() {
var disableWebhookConfigInstaller bool
flag.BoolVar(&disableWebhookConfigInstaller, "disable-webhook-config-installer", false,
"disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping")
flag.Parse()
logf.SetLogger(logf.ZapLogger(false))
entryLog := log.WithName("entrypoint")
// Setup a Manager
entryLog.Info("setting up manager")
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
if err != nil {
entryLog.Error(err, "unable to set up overall controller manager")
os.Exit(1)
}
// Setup a new controller to Reconciler ReplicaSets
entryLog.Info("Setting up controller")
c, err := controller.New("foo-controller", mgr, controller.Options{
Reconciler: &reconcileReplicaSet{client: mgr.GetClient(), log: log.WithName("reconciler")},
})
if err != nil {
entryLog.Error(err, "unable to set up individual controller")
os.Exit(1)
}
// Watch ReplicaSets and enqueue ReplicaSet object key
if err := c.Watch(&source.Kind{Type: &appsv1.ReplicaSet{}}, &handler.EnqueueRequestForObject{}); err != nil {
entryLog.Error(err, "unable to watch ReplicaSets")
os.Exit(1)
}
// Watch Pods and enqueue owning ReplicaSet key
if err := c.Watch(&source.Kind{Type: &corev1.Pod{}},
&handler.EnqueueRequestForOwner{OwnerType: &appsv1.ReplicaSet{}, IsController: true}); err != nil {
entryLog.Error(err, "unable to watch Pods")
os.Exit(1)
}
// Setup webhooks
entryLog.Info("setting up webhooks")
mutatingWebhook, err := builder.NewWebhookBuilder().
Name("mutating.k8s.io").
Mutating().
Operations(admissionregistrationv1beta1.Create, admissionregistrationv1beta1.Update).
WithManager(mgr).
ForType(&corev1.Pod{}).
Handlers(&podAnnotator{}).
Build()
if err != nil {
entryLog.Error(err, "unable to setup mutating webhook")
os.Exit(1)
}
validatingWebhook, err := builder.NewWebhookBuilder().
Name("validating.k8s.io").
Validating().
Operations(admissionregistrationv1beta1.Create, admissionregistrationv1beta1.Update).
WithManager(mgr).
ForType(&corev1.Pod{}).
Handlers(&podValidator{}).
Build()
if err != nil {
entryLog.Error(err, "unable to setup validating webhook")
os.Exit(1)
}
entryLog.Info("setting up webhook server")
as, err := webhook.NewServer("foo-admission-server", mgr, webhook.ServerOptions{
Port: 9876,
CertDir: "/tmp/cert",
DisableWebhookConfigInstaller: &disableWebhookConfigInstaller,
BootstrapOptions: &webhook.BootstrapOptions{
Secret: &apitypes.NamespacedName{
Namespace: "default",
Name: "foo-admission-server-secret",
},
Service: &webhook.Service{
Namespace: "default",
Name: "foo-admission-server-service",
// Selectors should select the pods that runs this webhook server.
Selectors: map[string]string{
"app": "foo-admission-server",
},
},
},
})
if err != nil {
entryLog.Error(err, "unable to create a new webhook server")
os.Exit(1)
}
entryLog.Info("registering webhooks to the webhook server")
err = as.Register(mutatingWebhook, validatingWebhook)
if err != nil {
entryLog.Error(err, "unable to register webhooks in the admission server")
os.Exit(1)
}
entryLog.Info("starting manager")
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
entryLog.Error(err, "unable to run manager")
os.Exit(1)
}
}

View File

@ -0,0 +1,83 @@
/*
Copyright 2018 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 main
import (
"context"
"net/http"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/types"
)
// podAnnotator annotates Pods
type podAnnotator struct {
client client.Client
decoder types.Decoder
}
// Implement admission.Handler so the controller can handle admission request.
var _ admission.Handler = &podAnnotator{}
// podAnnotator adds an annotation to every incoming pods.
func (a *podAnnotator) Handle(ctx context.Context, req types.Request) types.Response {
pod := &corev1.Pod{}
err := a.decoder.Decode(req, pod)
if err != nil {
return admission.ErrorResponse(http.StatusBadRequest, err)
}
copy := pod.DeepCopy()
err = a.mutatePodsFn(ctx, copy)
if err != nil {
return admission.ErrorResponse(http.StatusInternalServerError, err)
}
return admission.PatchResponse(pod, copy)
}
// mutatePodsFn add an annotation to the given pod
func (a *podAnnotator) mutatePodsFn(ctx context.Context, pod *corev1.Pod) error {
if pod.Annotations == nil {
pod.Annotations = map[string]string{}
}
pod.Annotations["example-mutating-admission-webhook"] = "foo"
return nil
}
// podValidator implements inject.Client.
// A client will be automatically injected.
var _ inject.Client = &podValidator{}
// InjectClient injects the client.
func (v *podAnnotator) InjectClient(c client.Client) error {
v.client = c
return nil
}
// podValidator implements inject.Decoder.
// A decoder will be automatically injected.
var _ inject.Decoder = &podValidator{}
// InjectDecoder injects the decoder.
func (v *podAnnotator) InjectDecoder(d types.Decoder) error {
v.decoder = d
return nil
}

View File

@ -0,0 +1,89 @@
/*
Copyright 2018 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 main
import (
"context"
"fmt"
"net/http"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/types"
)
// podValidator validates Pods
type podValidator struct {
client client.Client
decoder types.Decoder
}
// Implement admission.Handler so the controller can handle admission request.
var _ admission.Handler = &podValidator{}
// podValidator admits a pod iff a specific annotation exists.
func (v *podValidator) Handle(ctx context.Context, req types.Request) types.Response {
pod := &corev1.Pod{}
err := v.decoder.Decode(req, pod)
if err != nil {
return admission.ErrorResponse(http.StatusBadRequest, err)
}
allowed, reason, err := v.validatePodsFn(ctx, pod)
if err != nil {
return admission.ErrorResponse(http.StatusInternalServerError, err)
}
return admission.ValidationResponse(allowed, reason)
}
func (v *podValidator) validatePodsFn(ctx context.Context, pod *corev1.Pod) (bool, string, error) {
key := "example-mutating-admission-webhook"
anno, found := pod.Annotations[key]
switch {
case !found:
return found, fmt.Sprintf("failed to find annotation with key: %q", key), nil
case found && anno == "foo":
return found, "", nil
case found && anno != "foo":
return false,
fmt.Sprintf("the value associate with key %q is expected to be %q, but got %q", key, "foo", anno), nil
}
return false, "", nil
}
// podValidator implements inject.Client.
// A client will be automatically injected.
var _ inject.Client = &podValidator{}
// InjectClient injects the client.
func (v *podValidator) InjectClient(c client.Client) error {
v.client = c
return nil
}
// podValidator implements inject.Decoder.
// A decoder will be automatically injected.
var _ inject.Decoder = &podValidator{}
// InjectDecoder injects the decoder.
func (v *podValidator) InjectDecoder(d types.Decoder) error {
v.decoder = d
return nil
}

View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Copyright 2018 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.
set -e
hack_dir=$(dirname ${BASH_SOURCE})
source ${hack_dir}/common.sh
k8s_version=1.10.1
goarch=amd64
goos="unknown"
if [[ "$OSTYPE" == "linux-gnu" ]]; then
goos="linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
goos="darwin"
fi
if [[ "$goos" == "unknown" ]]; then
echo "OS '$OSTYPE' not supported. Aborting." >&2
exit 1
fi
tmp_root=/tmp
kb_root_dir=$tmp_root/kubebuilder
# Skip fetching and untaring the tools by setting the SKIP_FETCH_TOOLS variable
# in your environment to any value:
#
# $ SKIP_FETCH_TOOLS=1 ./test.sh
#
# If you skip fetching tools, this script will use the tools already on your
# machine, but rebuild the kubebuilder and kubebuilder-bin binaries.
SKIP_FETCH_TOOLS=${SKIP_FETCH_TOOLS:-""}
# fetch k8s API gen tools and make it available under kb_root_dir/bin.
function fetch_kb_tools {
if [ -n "$SKIP_FETCH_TOOLS" ]; then
return 0
fi
header_text "fetching tools"
kb_tools_archive_name="kubebuilder-tools-$k8s_version-$goos-$goarch.tar.gz"
kb_tools_download_url="https://storage.googleapis.com/kubebuilder-tools/$kb_tools_archive_name"
kb_tools_archive_path="$tmp_root/$kb_tools_archive_name"
if [ ! -f $kb_tools_archive_path ]; then
curl -sL ${kb_tools_download_url} -o "$kb_tools_archive_path"
fi
tar -zvxf "$kb_tools_archive_path" -C "$tmp_root/"
}
header_text "using tools"
which gometalinter.v2
fetch_kb_tools
setup_envs
${hack_dir}/verify.sh
${hack_dir}/test-all.sh
header_text "confirming example compiles (via go install)"
go install ./example
echo "passed"
exit 0

52
vendor/sigs.k8s.io/controller-runtime/hack/common.sh generated vendored Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Copyright 2018 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.
set -e
# Enable tracing in this script off by setting the TRACE variable in your
# environment to any value:
#
# $ TRACE=1 test.sh
TRACE=${TRACE:-""}
if [ -n "$TRACE" ]; then
set -x
fi
# Turn colors in this script off by setting the NO_COLOR variable in your
# environment to any value:
#
# $ NO_COLOR=1 test.sh
NO_COLOR=${NO_COLOR:-""}
if [ -z "$NO_COLOR" ]; then
header=$'\e[1;33m'
reset=$'\e[0m'
else
header=''
reset=''
fi
function header_text {
echo "$header$*$reset"
}
function setup_envs {
header_text "setting up env vars"
# Setup env vars
if [[ -z "${KUBEBUILDER_ASSETS}" ]]; then
export KUBEBUILDER_ASSETS=$kb_root_dir/bin
fi
}

37
vendor/sigs.k8s.io/controller-runtime/hack/test-all.sh generated vendored Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Copyright 2018 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.
set -e
source $(dirname ${BASH_SOURCE})/common.sh
setup_envs
header_text "running go test"
go test ./pkg/... -parallel 4
header_text "running coverage"
# Verify no coverage regressions have been introduced. Remove the exception list from here
# once the coverage has been brought back up
if [[ ! $(go test ./pkg/... -coverprofile cover.out -parallel 4 | grep -v "coverage: 100.0% of statements" | grep "controller-runtime/pkg " | grep -v "controller-runtime/pkg \|controller-runtime/pkg/recorder \|pkg/admission/certprovisioner \|pkg/internal/admission \|pkg/cache\|pkg/client \|pkg/event \|pkg/client/config \|pkg/controller/controllertest \|pkg/reconcile/reconciletest \|pkg/test ") ]]; then
echo "ok"
else
go test ./pkg/... -coverprofile cover.out -parallel 4 | grep -v "coverage: 100.0% of statements" | grep "controller-runtime/pkg " | grep -v "controller-runtime/pkg \|controller-runtime/pkg/recorder \|pkg/admission/certprovisioner \|pkg/internal/admission \|pkg/cache\|pkg/client \|pkg/event \|pkg/client/config \|pkg/controller/controllertest \|pkg/reconcile/reconciletest \|pkg/test "
echo "missing test coverage"
exit 1
fi

57
vendor/sigs.k8s.io/controller-runtime/hack/verify.sh generated vendored Executable file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Copyright 2018 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.
set -e
source $(dirname ${BASH_SOURCE})/common.sh
header_text "running go vet"
go vet ./pkg/...
# go get is broken for golint. re-enable this once it is fixed.
#header_text "running golint"
#
#golint -set_exit_status ./pkg/...
header_text "running gometalinter.v2"
gometalinter.v2 --disable-all \
--deadline 5m \
--enable=misspell \
--enable=structcheck \
--enable=golint \
--enable=deadcode \
--enable=goimports \
--enable=errcheck \
--enable=varcheck \
--enable=goconst \
--enable=unparam \
--enable=ineffassign \
--enable=nakedret \
--enable=interfacer \
--enable=misspell \
--enable=gocyclo \
--line-length=170 \
--enable=lll \
--dupl-threshold=400 \
--enable=dupl \
--skip=atomic \
./pkg/...
# TODO: Enable these as we fix them to make them pass
# --enable=gosec \
# --enable=maligned \
# --enable=safesql \

View File

@ -0,0 +1,176 @@
/*
Copyright 2018 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 builder
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
// application is a simple Controller for a single API type. It will create a Manager for itself
// if one is not provided.
type application struct {
mgr manager.Manager
ctrl controller.Controller
}
// Supporting mocking out functions for testing
var getConfig = config.GetConfig
var newController = controller.New
var newManager = manager.New
var getGvk = apiutil.GVKForObject
// Builder builds an Application Controller (e.g. Operator) and returns a manager.Manager to start it.
type Builder struct {
apiType runtime.Object
mgr manager.Manager
predicates []predicate.Predicate
managedObjects []runtime.Object
config *rest.Config
ctrl controller.Controller
}
// SimpleController returns a new Builder
func SimpleController() *Builder {
return &Builder{}
}
// ForType sets the ForType that generates other types
func (b *Builder) ForType(apiType runtime.Object) *Builder {
b.apiType = apiType
return b
}
// Owns configures the Application Controller to respond to create / delete / update events for objects it managedObjects
// - e.g. creates. apiType is an empty instance of an object matching the managed object type.
func (b *Builder) Owns(apiType runtime.Object) *Builder {
b.managedObjects = append(b.managedObjects, apiType)
return b
}
// WithConfig sets the Config to use for configuring clients. Defaults to the in-cluster config or to ~/.kube/config.
func (b *Builder) WithConfig(config *rest.Config) *Builder {
b.config = config
return b
}
// WithManager sets the Manager to use for registering the Controller. Defaults to a new manager.Manager.
func (b *Builder) WithManager(m manager.Manager) *Builder {
b.mgr = m
return b
}
// WithEventFilter sets the event filters, to filter which create/update/delete/generic events eventually
// trigger reconciliations. For example, filtering on whether the resource version has changed.
// Defaults to the empty list.
func (b *Builder) WithEventFilter(p predicate.Predicate) *Builder {
b.predicates = append(b.predicates, p)
return b
}
// Build builds the Application Controller and returns the Manager used to start it.
func (b *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) {
if r == nil {
return nil, fmt.Errorf("must call WithReconciler to set Reconciler")
}
// Set the Config
if err := b.doConfig(); err != nil {
return nil, err
}
// Set the Manager
if err := b.doManager(); err != nil {
return nil, err
}
// Set the Controller
if err := b.doController(r); err != nil {
return nil, err
}
a := &application{mgr: b.mgr, ctrl: b.ctrl}
// Reconcile type
s := &source.Kind{Type: b.apiType}
h := &handler.EnqueueRequestForObject{}
err := a.ctrl.Watch(s, h, b.predicates...)
if err != nil {
return nil, err
}
// Watch the managed types
for _, t := range b.managedObjects {
s := &source.Kind{Type: t}
h := &handler.EnqueueRequestForOwner{
OwnerType: b.apiType,
IsController: true,
}
if err := a.ctrl.Watch(s, h, b.predicates...); err != nil {
return nil, err
}
}
return a.mgr, nil
}
func (b *Builder) doConfig() error {
if b.config != nil {
return nil
}
var err error
b.config, err = getConfig()
return err
}
func (b *Builder) doManager() error {
if b.mgr != nil {
return nil
}
var err error
b.mgr, err = newManager(b.config, manager.Options{})
return err
}
func (b *Builder) getControllerName() (string, error) {
gvk, err := getGvk(b.apiType, b.mgr.GetScheme())
if err != nil {
return "", err
}
name := fmt.Sprintf("%s-application", strings.ToLower(gvk.Kind))
return name, nil
}
func (b *Builder) doController(r reconcile.Reconciler) error {
name, err := b.getControllerName()
if err != nil {
return err
}
b.ctrl, err = newController(name, b.mgr, controller.Options{Reconciler: r})
return err
}

View File

@ -0,0 +1,216 @@
/*
Copyright 2018 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 builder
import (
"context"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ = Describe("application", func() {
var stop chan struct{}
BeforeEach(func() {
stop = make(chan struct{})
getConfig = func() (*rest.Config, error) { return cfg, nil }
newController = controller.New
newManager = manager.New
getGvk = apiutil.GVKForObject
})
AfterEach(func() {
close(stop)
})
noop := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { return reconcile.Result{}, nil })
Describe("New", func() {
It("should return success if given valid objects", func() {
instance, err := SimpleController().
ForType(&appsv1.ReplicaSet{}).
Owns(&appsv1.ReplicaSet{}).
Build(noop)
Expect(err).NotTo(HaveOccurred())
Expect(instance).NotTo(BeNil())
})
It("should return an error if the Config is invalid", func() {
getConfig = func() (*rest.Config, error) { return cfg, fmt.Errorf("expected error") }
instance, err := SimpleController().
ForType(&appsv1.ReplicaSet{}).
Owns(&appsv1.ReplicaSet{}).
Build(noop)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("expected error"))
Expect(instance).To(BeNil())
})
It("should return an error if there is no GVK for an object", func() {
instance, err := SimpleController().
ForType(&fakeType{}).
Owns(&appsv1.ReplicaSet{}).
Build(noop)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("no kind is registered for the type builder.fakeType"))
Expect(instance).To(BeNil())
instance, err = SimpleController().
ForType(&appsv1.ReplicaSet{}).
Owns(&fakeType{}).
Build(noop)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("no kind is registered for the type builder.fakeType"))
Expect(instance).To(BeNil())
})
It("should return an error if it cannot create the manager", func() {
newManager = func(config *rest.Config, options manager.Options) (manager.Manager, error) {
return nil, fmt.Errorf("expected error")
}
instance, err := SimpleController().
ForType(&appsv1.ReplicaSet{}).
Owns(&appsv1.ReplicaSet{}).
Build(noop)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("expected error"))
Expect(instance).To(BeNil())
})
It("should return an error if it cannot create the controller", func() {
newController = func(name string, mgr manager.Manager, options controller.Options) (
controller.Controller, error) {
return nil, fmt.Errorf("expected error")
}
instance, err := SimpleController().
ForType(&appsv1.ReplicaSet{}).
Owns(&appsv1.ReplicaSet{}).
Build(noop)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("expected error"))
Expect(instance).To(BeNil())
})
})
Describe("Start", func() {
It("should Reconcile objects", func(done Done) {
By("Creating the application")
ch := make(chan reconcile.Request)
fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) {
defer GinkgoRecover()
ch <- req
return reconcile.Result{}, nil
})
instance, err := SimpleController().ForType(&appsv1.Deployment{}).
WithConfig(cfg).
Owns(&appsv1.ReplicaSet{}).
Build(fn)
Expect(err).NotTo(HaveOccurred())
By("Starting the application")
go func() {
defer GinkgoRecover()
Expect(instance.Start(stop)).NotTo(HaveOccurred())
By("Stopping the application")
}()
By("Creating a Deployment")
// Expect a Reconcile when the Deployment is managedObjects.
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "deploy-name",
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
},
},
}
err = instance.GetClient().Create(context.TODO(), dep)
Expect(err).NotTo(HaveOccurred())
By("Waiting for the Deployment Reconcile")
Expect(<-ch).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "default", Name: "deploy-name"}}))
By("Creating a ReplicaSet")
// Expect a Reconcile when an Owned object is managedObjects.
t := true
rs := &appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "rs-name",
Labels: dep.Spec.Selector.MatchLabels,
OwnerReferences: []metav1.OwnerReference{
{
Name: "deploy-name",
Kind: "Deployment",
APIVersion: "apps/v1",
Controller: &t,
UID: dep.UID,
},
},
},
Spec: appsv1.ReplicaSetSpec{
Selector: dep.Spec.Selector,
Template: dep.Spec.Template,
},
}
err = instance.GetClient().Create(context.TODO(), rs)
Expect(err).NotTo(HaveOccurred())
By("Waiting for the ReplicaSet Reconcile")
Expect(<-ch).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "default", Name: "deploy-name"}}))
close(done)
}, 10)
})
})
var _ runtime.Object = &fakeType{}
type fakeType struct{}
func (*fakeType) GetObjectKind() schema.ObjectKind { return nil }
func (*fakeType) DeepCopyObject() runtime.Object { return nil }

View File

@ -0,0 +1,51 @@
/*
Copyright 2018 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 builder
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestSource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "application Suite", []Reporter{envtest.NewlineReporter{}})
}
var testenv *envtest.Environment
var cfg *rest.Config
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
testenv = &envtest.Environment{}
var err error
cfg, err = testenv.Start()
Expect(err).NotTo(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
testenv.Stop()
})

View File

@ -0,0 +1,22 @@
/*
Copyright 2018 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 builder provides wraps other controller-runtime libraries and exposes simple
// patterns for building common Controllers.
//
// Projects built with the builder package can trivially be rebased on top of the underlying
// packages if the project requires more customized behavior in the future.
package builder

View File

@ -0,0 +1,98 @@
/*
Copyright 2018 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 builder_test
import (
"context"
"fmt"
"os"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
)
// NB: don't call SetLogger in init(), or else you'll mess up logging in the main suite.
var log = logf.Log.WithName("builder-examples")
// This example creates a simple application Controller that is configured for ReplicaSets and Pods.
//
// * Create a new application for ReplicaSets that manages Pods owned by the ReplicaSet and calls into
// ReplicaSetReconciler.
//
// * Start the application.
func ExampleBuilder() {
rs, err := builder.SimpleController().
ForType(&appsv1.ReplicaSet{}). // ReplicaSet is the Application API
Owns(&corev1.Pod{}). // ReplicaSet owns Pods created by it
Build(&ReplicaSetReconciler{}) // Build
if err != nil {
log.Error(err, "Unable to build controller")
os.Exit(1)
}
if err := rs.Start(signals.SetupSignalHandler()); err != nil {
log.Error(err, "Unable to run controller")
os.Exit(1)
}
}
// ReplicaSetReconciler is a simple Controller example implementation.
type ReplicaSetReconciler struct {
client.Client
}
// Implement the business logic:
// This function will be called when there is a change to a ReplicaSet or a Pod with an OwnerReference
// to a ReplicaSet.
//
// * Read the ReplicaSet
// * Read the Pods
// * Set a Label on the ReplicaSet with the Pod count
func (a *ReplicaSetReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
// Read the ReplicaSet
rs := &appsv1.ReplicaSet{}
err := a.Get(context.TODO(), req.NamespacedName, rs)
if err != nil {
return reconcile.Result{}, err
}
// List the Pods matching the PodTemplate Labels
pods := &corev1.PodList{}
err = a.List(context.TODO(), client.InNamespace(req.Namespace).MatchingLabels(rs.Spec.Template.Labels), pods)
if err != nil {
return reconcile.Result{}, err
}
// Update the ReplicaSet
rs.Labels["pod-count"] = fmt.Sprintf("%v", len(pods.Items))
err = a.Update(context.TODO(), rs)
if err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
func (a *ReplicaSetReconciler) InjectClient(c client.Client) error {
a.Client = c
return nil
}

View File

@ -0,0 +1,121 @@
/*
Copyright 2018 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 cache
import (
"fmt"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"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"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
var log = logf.KBLog.WithName("object-cache")
// Cache implements CacheReader by reading objects from a cache populated by InformersMap
type Cache interface {
// Cache implements the client CacheReader
client.Reader
// Cache implements InformersMap
Informers
}
// Informers knows how to create or fetch informers for different group-version-kinds.
// It's safe to call GetInformer from multiple threads.
type Informers interface {
// GetInformer fetches or constructs an informer for the given object that corresponds to a single
// API kind and resource.
GetInformer(obj runtime.Object) (toolscache.SharedIndexInformer, error)
// GetInformerForKind is similar to GetInformer, except that it takes a group-version-kind, instead
// of the underlying object.
GetInformerForKind(gvk schema.GroupVersionKind) (toolscache.SharedIndexInformer, error)
// Start runs all the informers known to this cache until the given channel is closed.
// It blocks.
Start(stopCh <-chan struct{}) error
// WaitForCacheSync waits for all the caches to sync. Returns false if it could not sync a cache.
WaitForCacheSync(stop <-chan struct{}) bool
// IndexField adds an index with the given field name on the given object type
// by using the given function to extract the value for that field. If you want
// compatibility with the Kubernetes API server, only return one key, and only use
// fields that the API server supports. Otherwise, you can return multiple keys,
// and "equality" in the field selector means that at least one key matches the value.
IndexField(obj runtime.Object, field string, extractValue client.IndexerFunc) error
}
// Options are the optional arguments for creating a new InformersMap object
type Options struct {
// Scheme is the scheme to use for mapping objects to GroupVersionKinds
Scheme *runtime.Scheme
// Mapper is the RESTMapper to use for mapping GroupVersionKinds to Resources
Mapper meta.RESTMapper
// Resync is the resync period. Defaults to defaultResyncTime.
Resync *time.Duration
// Namespace restricts the cache's ListWatch to the desired namespace
// Default watches all namespaces
Namespace string
}
var defaultResyncTime = 10 * time.Hour
// New initializes and returns a new Cache
func New(config *rest.Config, opts Options) (Cache, error) {
opts, err := defaultOpts(config, opts)
if err != nil {
return nil, err
}
im := internal.NewInformersMap(config, opts.Scheme, opts.Mapper, *opts.Resync, opts.Namespace)
return &informerCache{InformersMap: im}, nil
}
func defaultOpts(config *rest.Config, opts Options) (Options, error) {
// Use the default Kubernetes Scheme if unset
if opts.Scheme == nil {
opts.Scheme = scheme.Scheme
}
// Construct a new Mapper if unset
if opts.Mapper == nil {
var err error
opts.Mapper, err = apiutil.NewDiscoveryRESTMapper(config)
if err != nil {
log.WithName("setup").Error(err, "Failed to get API Group-Resources")
return opts, fmt.Errorf("could not create RESTMapper from config")
}
}
// Default the resync period to 10 hours if unset
if opts.Resync == nil {
opts.Resync = &defaultResyncTime
}
return opts, nil
}

View File

@ -0,0 +1,56 @@
/*
Copyright 2018 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 cache_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestSource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "Cache Suite", []Reporter{envtest.NewlineReporter{}})
}
var testenv *envtest.Environment
var cfg *rest.Config
var clientset *kubernetes.Clientset
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
testenv = &envtest.Environment{}
var err error
cfg, err = testenv.Start()
Expect(err).NotTo(HaveOccurred())
clientset, err = kubernetes.NewForConfig(cfg)
Expect(err).NotTo(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
testenv.Stop()
})

View File

@ -0,0 +1,656 @@
/*
Copyright 2018 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 cache_test
import (
"context"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
kcorev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
kmetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
kscheme "k8s.io/client-go/kubernetes/scheme"
kcache "k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const testNamespaceOne = "test-namespace-1"
const testNamespaceTwo = "test-namespace-2"
// TODO(community): Pull these helper functions into testenv.
// Restart policy is included to allow indexing on that field.
func createPod(name, namespace string, restartPolicy kcorev1.RestartPolicy) runtime.Object {
three := int64(3)
pod := &kcorev1.Pod{
ObjectMeta: kmetav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: map[string]string{
"test-label": name,
},
},
Spec: kcorev1.PodSpec{
Containers: []kcorev1.Container{{Name: "nginx", Image: "nginx"}},
RestartPolicy: restartPolicy,
ActiveDeadlineSeconds: &three,
},
}
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
err = cl.Create(context.Background(), pod)
Expect(err).NotTo(HaveOccurred())
return pod
}
func deletePod(pod runtime.Object) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
err = cl.Delete(context.Background(), pod)
Expect(err).NotTo(HaveOccurred())
}
var _ = Describe("Informer Cache", func() {
var (
informerCache cache.Cache
stop chan struct{}
knownPod1 runtime.Object
knownPod2 runtime.Object
knownPod3 runtime.Object
)
BeforeEach(func() {
stop = make(chan struct{})
Expect(cfg).NotTo(BeNil())
By("creating three pods")
// Includes restart policy since these objects are indexed on this field.
knownPod1 = createPod("test-pod-1", testNamespaceOne, kcorev1.RestartPolicyNever)
knownPod2 = createPod("test-pod-2", testNamespaceTwo, kcorev1.RestartPolicyAlways)
knownPod3 = createPod("test-pod-3", testNamespaceTwo, kcorev1.RestartPolicyOnFailure)
By("creating the informer cache")
var err error
informerCache, err = cache.New(cfg, cache.Options{})
Expect(err).NotTo(HaveOccurred())
By("running the cache and waiting for it to sync")
go func() {
defer GinkgoRecover()
Expect(informerCache.Start(stop)).To(Succeed())
}()
Expect(informerCache.WaitForCacheSync(stop)).To(BeTrue())
})
AfterEach(func() {
By("cleaning up created pods")
deletePod(knownPod1)
deletePod(knownPod2)
deletePod(knownPod3)
close(stop)
})
Describe("as a Reader", func() {
Context("with structured objects", func() {
It("should be able to list objects that haven't been watched previously", func() {
By("listing all services in the cluster")
listObj := &kcorev1.ServiceList{}
Expect(informerCache.List(context.Background(), nil, listObj)).To(Succeed())
By("verifying that the returned list contains the Kubernetes service")
// NB: kubernetes default service is automatically created in testenv.
Expect(listObj.Items).NotTo(BeEmpty())
hasKubeService := false
for _, svc := range listObj.Items {
if svc.Namespace == "default" && svc.Name == "kubernetes" {
hasKubeService = true
break
}
}
Expect(hasKubeService).To(BeTrue())
})
It("should be able to get objects that haven't been watched previously", func() {
By("getting the Kubernetes service")
svc := &kcorev1.Service{}
svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"}
Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed())
By("verifying that the returned service looks reasonable")
Expect(svc.Name).To(Equal("kubernetes"))
Expect(svc.Namespace).To(Equal("default"))
})
It("should support filtering by labels in a single namespace", func() {
By("listing pods with a particular label")
// NB: each pod has a "test-label": <pod-name>
out := kcorev1.PodList{}
lo := &client.ListOptions{}
lo.InNamespace(testNamespaceTwo)
lo.MatchingLabels(map[string]string{"test-label": "test-pod-2"})
Expect(informerCache.List(context.Background(), lo, &out)).To(Succeed())
By("verifying the returned pods have the correct label")
Expect(out.Items).NotTo(BeEmpty())
Expect(out.Items).Should(HaveLen(1))
actual := out.Items[0]
Expect(actual.Labels["test-label"]).To(Equal("test-pod-2"))
})
It("should support filtering by labels from multiple namespaces", func() {
By("creating another pod with the same label but different namespace")
anotherPod := createPod("test-pod-2", testNamespaceOne, kcorev1.RestartPolicyAlways)
By("listing pods with a particular label")
// NB: each pod has a "test-label": <pod-name>
out := kcorev1.PodList{}
labels := map[string]string{"test-label": "test-pod-2"}
lo := &client.ListOptions{}
lo.MatchingLabels(labels)
Expect(informerCache.List(context.Background(), lo, &out)).To(Succeed())
By("verifying multiple pods with the same label in different namespaces are returned")
Expect(out.Items).NotTo(BeEmpty())
Expect(out.Items).Should(HaveLen(2))
for _, actual := range out.Items {
Expect(actual.Labels["test-label"]).To(Equal("test-pod-2"))
}
deletePod(anotherPod)
})
It("should be able to list objects by namespace", func() {
By("listing pods in test-namespace-1")
listObj := &kcorev1.PodList{}
lo := &client.ListOptions{}
lo.InNamespace(testNamespaceOne)
Expect(informerCache.List(context.Background(), lo, listObj)).To(Succeed())
By("verifying that the returned pods are in test-namespace-1")
Expect(listObj.Items).NotTo(BeEmpty())
Expect(listObj.Items).Should(HaveLen(1))
actual := listObj.Items[0]
Expect(actual.Namespace).To(Equal(testNamespaceOne))
})
It("should be able to restrict cache to a namespace", func() {
By("creating a namespaced cache")
namespacedCache, err := cache.New(cfg, cache.Options{Namespace: testNamespaceOne})
Expect(err).NotTo(HaveOccurred())
By("running the cache and waiting for it to sync")
go func() {
defer GinkgoRecover()
Expect(namespacedCache.Start(stop)).To(Succeed())
}()
Expect(namespacedCache.WaitForCacheSync(stop)).NotTo(BeFalse())
By("listing pods in all namespaces")
out := &kcorev1.PodList{}
Expect(namespacedCache.List(context.Background(), nil, out)).To(Succeed())
By("verifying the returned pod is from the watched namespace")
Expect(out.Items).NotTo(BeEmpty())
Expect(out.Items).Should(HaveLen(1))
Expect(out.Items[0].Namespace).To(Equal(testNamespaceOne))
By("listing all namespaces - should still be able to get a cluster-scoped resource")
namespaceList := &kcorev1.NamespaceList{}
Expect(namespacedCache.List(context.Background(), nil, namespaceList)).To(Succeed())
By("verifying the namespace list is not empty")
Expect(namespaceList.Items).NotTo(BeEmpty())
})
It("should deep copy the object unless told otherwise", func() {
By("retrieving a specific pod from the cache")
out := &kcorev1.Pod{}
podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo}
Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed())
By("verifying the retrieved pod is equal to a known pod")
Expect(out).To(Equal(knownPod2))
By("altering a field in the retrieved pod")
*out.Spec.ActiveDeadlineSeconds = 4
By("verifying the pods are no longer equal")
Expect(out).NotTo(Equal(knownPod2))
})
It("should return an error if the object is not found", func() {
By("getting a service that does not exists")
svc := &kcorev1.Service{}
svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"}
By("verifying that an error is returned")
err := informerCache.Get(context.Background(), svcKey, svc)
Expect(err).To(HaveOccurred())
Expect(errors.IsNotFound(err)).To(BeTrue())
})
})
Context("with unstructured objects", func() {
It("should be able to list objects that haven't been watched previously", func() {
By("listing all services in the cluster")
listObj := &unstructured.UnstructuredList{}
listObj.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "ServiceList",
})
err := informerCache.List(context.Background(), nil, listObj)
Expect(err).To(Succeed())
By("verifying that the returned list contains the Kubernetes service")
// NB: kubernetes default service is automatically created in testenv.
Expect(listObj.Items).NotTo(BeEmpty())
hasKubeService := false
for _, svc := range listObj.Items {
if svc.GetNamespace() == "default" && svc.GetName() == "kubernetes" {
hasKubeService = true
break
}
}
Expect(hasKubeService).To(BeTrue())
})
It("should be able to get objects that haven't been watched previously", func() {
By("getting the Kubernetes service")
svc := &unstructured.Unstructured{}
svc.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Service",
})
svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"}
Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed())
By("verifying that the returned service looks reasonable")
Expect(svc.GetName()).To(Equal("kubernetes"))
Expect(svc.GetNamespace()).To(Equal("default"))
})
It("should support filtering by labels in a single namespace", func() {
By("listing pods with a particular label")
// NB: each pod has a "test-label": <pod-name>
out := unstructured.UnstructuredList{}
out.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "PodList",
})
lo := &client.ListOptions{}
lo.InNamespace(testNamespaceTwo)
lo.MatchingLabels(map[string]string{"test-label": "test-pod-2"})
err := informerCache.List(context.Background(), lo, &out)
Expect(err).To(Succeed())
By("verifying the returned pods have the correct label")
Expect(out.Items).NotTo(BeEmpty())
Expect(out.Items).Should(HaveLen(1))
actual := out.Items[0]
Expect(actual.GetLabels()["test-label"]).To(Equal("test-pod-2"))
})
It("should support filtering by labels from multiple namespaces", func() {
By("creating another pod with the same label but different namespace")
anotherPod := createPod("test-pod-2", testNamespaceOne, kcorev1.RestartPolicyAlways)
By("listing pods with a particular label")
// NB: each pod has a "test-label": <pod-name>
out := unstructured.UnstructuredList{}
out.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "PodList",
})
labels := map[string]string{"test-label": "test-pod-2"}
lo := &client.ListOptions{}
lo.MatchingLabels(labels)
err := informerCache.List(context.Background(), lo, &out)
Expect(err).To(Succeed())
By("verifying multiple pods with the same label in different namespaces are returned")
Expect(out.Items).NotTo(BeEmpty())
Expect(out.Items).Should(HaveLen(2))
for _, actual := range out.Items {
Expect(actual.GetLabels()["test-label"]).To(Equal("test-pod-2"))
}
deletePod(anotherPod)
})
It("should be able to list objects by namespace", func() {
By("listing pods in test-namespace-1")
listObj := &unstructured.UnstructuredList{}
listObj.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "PodList",
})
lo := &client.ListOptions{}
lo.InNamespace(testNamespaceOne)
err := informerCache.List(context.Background(), lo, listObj)
Expect(err).To(Succeed())
By("verifying that the returned pods are in test-namespace-1")
Expect(listObj.Items).NotTo(BeEmpty())
Expect(listObj.Items).Should(HaveLen(1))
actual := listObj.Items[0]
Expect(actual.GetNamespace()).To(Equal(testNamespaceOne))
})
It("should be able to restrict cache to a namespace", func() {
By("creating a namespaced cache")
namespacedCache, err := cache.New(cfg, cache.Options{Namespace: testNamespaceOne})
Expect(err).NotTo(HaveOccurred())
By("running the cache and waiting for it to sync")
go func() {
defer GinkgoRecover()
Expect(namespacedCache.Start(stop)).To(Succeed())
}()
Expect(namespacedCache.WaitForCacheSync(stop)).NotTo(BeFalse())
By("listing pods in all namespaces")
out := &unstructured.UnstructuredList{}
out.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "PodList",
})
Expect(namespacedCache.List(context.Background(), nil, out)).To(Succeed())
By("verifying the returned pod is from the watched namespace")
Expect(out.Items).NotTo(BeEmpty())
Expect(out.Items).Should(HaveLen(1))
Expect(out.Items[0].GetNamespace()).To(Equal(testNamespaceOne))
By("listing all namespaces - should still be able to get a cluster-scoped resource")
namespaceList := &unstructured.UnstructuredList{}
namespaceList.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "NamespaceList",
})
Expect(namespacedCache.List(context.Background(), nil, namespaceList)).To(Succeed())
By("verifying the namespace list is not empty")
Expect(namespaceList.Items).NotTo(BeEmpty())
})
It("should deep copy the object unless told otherwise", func() {
By("retrieving a specific pod from the cache")
out := &unstructured.Unstructured{}
out.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Pod",
})
uKnownPod2 := &unstructured.Unstructured{}
kscheme.Scheme.Convert(knownPod2, uKnownPod2, nil)
podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo}
Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed())
By("verifying the retrieved pod is equal to a known pod")
Expect(out).To(Equal(uKnownPod2))
By("altering a field in the retrieved pod")
m, _ := out.Object["spec"].(map[string]interface{})
m["activeDeadlineSeconds"] = 4
By("verifying the pods are no longer equal")
Expect(out).NotTo(Equal(knownPod2))
})
It("should return an error if the object is not found", func() {
By("getting a service that does not exists")
svc := &unstructured.Unstructured{}
svc.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Service",
})
svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"}
By("verifying that an error is returned")
err := informerCache.Get(context.Background(), svcKey, svc)
Expect(err).To(HaveOccurred())
Expect(errors.IsNotFound(err)).To(BeTrue())
})
})
})
Describe("as an Informer", func() {
Context("with structured objects", func() {
It("should be able to get informer for the object", func(done Done) {
By("getting a shared index informer for a pod")
pod := &kcorev1.Pod{
ObjectMeta: kmetav1.ObjectMeta{
Name: "informer-obj",
Namespace: "default",
},
Spec: kcorev1.PodSpec{
Containers: []kcorev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
}
sii, err := informerCache.GetInformer(pod)
Expect(err).NotTo(HaveOccurred())
Expect(sii).NotTo(BeNil())
Expect(sii.HasSynced()).To(BeTrue())
By("adding an event handler listening for object creation which sends the object to a channel")
out := make(chan interface{})
addFunc := func(obj interface{}) {
out <- obj
}
sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc})
By("adding an object")
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl.Create(context.Background(), pod)).To(Succeed())
By("verifying the object is received on the channel")
Eventually(out).Should(Receive(Equal(pod)))
close(done)
})
// TODO: Add a test for when GVK is not in Scheme. Does code support informer for unstructured object?
It("should be able to get an informer by group/version/kind", func(done Done) {
By("getting an shared index informer for gvk = core/v1/pod")
gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}
sii, err := informerCache.GetInformerForKind(gvk)
Expect(err).NotTo(HaveOccurred())
Expect(sii).NotTo(BeNil())
Expect(sii.HasSynced()).To(BeTrue())
By("adding an event handler listening for object creation which sends the object to a channel")
out := make(chan interface{})
addFunc := func(obj interface{}) {
out <- obj
}
sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc})
By("adding an object")
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
pod := &kcorev1.Pod{
ObjectMeta: kmetav1.ObjectMeta{
Name: "informer-gvk",
Namespace: "default",
},
Spec: kcorev1.PodSpec{
Containers: []kcorev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
}
Expect(cl.Create(context.Background(), pod)).To(Succeed())
By("verifying the object is received on the channel")
Eventually(out).Should(Receive(Equal(pod)))
close(done)
})
It("should be able to index an object field then retrieve objects by that field", func() {
By("creating the cache")
informer, err := cache.New(cfg, cache.Options{})
Expect(err).NotTo(HaveOccurred())
By("indexing the restartPolicy field of the Pod object before starting")
pod := &kcorev1.Pod{}
indexFunc := func(obj runtime.Object) []string {
return []string{string(obj.(*kcorev1.Pod).Spec.RestartPolicy)}
}
Expect(informer.IndexField(pod, "spec.restartPolicy", indexFunc)).To(Succeed())
By("running the cache and waiting for it to sync")
go func() {
defer GinkgoRecover()
Expect(informer.Start(stop)).To(Succeed())
}()
Expect(informer.WaitForCacheSync(stop)).NotTo(BeFalse())
By("listing Pods with restartPolicyOnFailure")
listObj := &kcorev1.PodList{}
lo := &client.ListOptions{}
lo.MatchingField("spec.restartPolicy", "OnFailure")
Expect(informer.List(context.Background(), lo, listObj)).To(Succeed())
By("verifying that the returned pods have correct restart policy")
Expect(listObj.Items).NotTo(BeEmpty())
Expect(listObj.Items).Should(HaveLen(1))
actual := listObj.Items[0]
Expect(actual.Name).To(Equal("test-pod-3"))
})
})
Context("with unstructured objects", func() {
It("should be able to get informer for the object", func(done Done) {
By("getting a shared index informer for a pod")
pod := &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
"containers": []map[string]interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx",
},
},
},
},
}
pod.SetName("informer-obj2")
pod.SetNamespace("default")
pod.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Pod",
})
sii, err := informerCache.GetInformer(pod)
Expect(err).NotTo(HaveOccurred())
Expect(sii).NotTo(BeNil())
Expect(sii.HasSynced()).To(BeTrue())
By("adding an event handler listening for object creation which sends the object to a channel")
out := make(chan interface{})
addFunc := func(obj interface{}) {
out <- obj
}
sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc})
By("adding an object")
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl.Create(context.Background(), pod)).To(Succeed())
By("verifying the object is received on the channel")
Eventually(out).Should(Receive(Equal(pod)))
close(done)
}, 3)
It("should be able to index an object field then retrieve objects by that field", func() {
By("creating the cache")
informer, err := cache.New(cfg, cache.Options{})
Expect(err).NotTo(HaveOccurred())
By("indexing the restartPolicy field of the Pod object before starting")
pod := &unstructured.Unstructured{}
pod.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Pod",
})
indexFunc := func(obj runtime.Object) []string {
s, ok := obj.(*unstructured.Unstructured).Object["spec"]
if !ok {
return []string{}
}
m, ok := s.(map[string]interface{})
if !ok {
return []string{}
}
return []string{fmt.Sprintf("%v", m["restartPolicy"])}
}
Expect(informer.IndexField(pod, "spec.restartPolicy", indexFunc)).To(Succeed())
By("running the cache and waiting for it to sync")
go func() {
defer GinkgoRecover()
Expect(informer.Start(stop)).To(Succeed())
}()
Expect(informer.WaitForCacheSync(stop)).NotTo(BeFalse())
By("listing Pods with restartPolicyOnFailure")
listObj := &unstructured.UnstructuredList{}
listObj.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "PodList",
})
lo := &client.ListOptions{}
lo.MatchingField("spec.restartPolicy", "OnFailure")
err = informer.List(context.Background(), lo, listObj)
Expect(err).To(Succeed())
By("verifying that the returned pods have correct restart policy")
Expect(listObj.Items).NotTo(BeEmpty())
Expect(listObj.Items).Should(HaveLen(1))
actual := listObj.Items[0]
Expect(actual.GetName()).To(Equal("test-pod-3"))
}, 3)
})
})
})

View File

@ -0,0 +1,178 @@
/*
Copyright 2018 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 cache
import (
"context"
"fmt"
"reflect"
"strings"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"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"
)
var (
_ Informers = &informerCache{}
_ client.Reader = &informerCache{}
_ Cache = &informerCache{}
)
// informerCache is a Kubernetes Object cache populated from InformersMap. informerCache wraps an InformersMap.
type informerCache struct {
*internal.InformersMap
}
// Get implements Reader
func (ip *informerCache) Get(ctx context.Context, key client.ObjectKey, out runtime.Object) error {
gvk, err := apiutil.GVKForObject(out, ip.Scheme)
if err != nil {
return err
}
cache, err := ip.InformersMap.Get(gvk, out)
if err != nil {
return err
}
return cache.Reader.Get(ctx, key, out)
}
// List implements Reader
func (ip *informerCache) List(ctx context.Context, opts *client.ListOptions, out runtime.Object) error {
gvk, err := apiutil.GVKForObject(out, ip.Scheme)
if err != nil {
return err
}
if !strings.HasSuffix(gvk.Kind, "List") {
return fmt.Errorf("non-list type %T (kind %q) passed as output", out, gvk)
}
// we need the non-list GVK, so chop off the "List" from the end of the kind
gvk.Kind = gvk.Kind[:len(gvk.Kind)-4]
_, isUnstructured := out.(*unstructured.UnstructuredList)
var cacheTypeObj runtime.Object
if isUnstructured {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(gvk)
cacheTypeObj = u
} else {
itemsPtr, err := apimeta.GetItemsPtr(out)
if err != nil {
return nil
}
// http://knowyourmeme.com/memes/this-is-fine
elemType := reflect.Indirect(reflect.ValueOf(itemsPtr)).Type().Elem()
cacheTypeValue := reflect.Zero(reflect.PtrTo(elemType))
var ok bool
cacheTypeObj, ok = cacheTypeValue.Interface().(runtime.Object)
if !ok {
return fmt.Errorf("cannot get cache for %T, its element %T is not a runtime.Object", out, cacheTypeValue.Interface())
}
}
cache, err := ip.InformersMap.Get(gvk, cacheTypeObj)
if err != nil {
return err
}
return cache.Reader.List(ctx, opts, out)
}
// GetInformerForKind returns the informer for the GroupVersionKind
func (ip *informerCache) GetInformerForKind(gvk schema.GroupVersionKind) (cache.SharedIndexInformer, error) {
// Map the gvk to an object
obj, err := ip.Scheme.New(gvk)
if err != nil {
return nil, err
}
i, err := ip.InformersMap.Get(gvk, obj)
if err != nil {
return nil, err
}
return i.Informer, err
}
// GetInformer returns the informer for the obj
func (ip *informerCache) GetInformer(obj runtime.Object) (cache.SharedIndexInformer, error) {
gvk, err := apiutil.GVKForObject(obj, ip.Scheme)
if err != nil {
return nil, err
}
i, err := ip.InformersMap.Get(gvk, obj)
if err != nil {
return nil, err
}
return i.Informer, err
}
// IndexField adds an indexer to the underlying cache, using extraction function to get
// value(s) from the given field. This index can then be used by passing a field selector
// to List. For one-to-one compatibility with "normal" field selectors, only return one value.
// The values may be anything. They will automatically be prefixed with the namespace of the
// given object, if present. The objects passed are guaranteed to be objects of the correct type.
func (ip *informerCache) IndexField(obj runtime.Object, field string, extractValue client.IndexerFunc) error {
informer, err := ip.GetInformer(obj)
if err != nil {
return err
}
return indexByField(informer.GetIndexer(), field, extractValue)
}
func indexByField(indexer cache.Indexer, field string, extractor client.IndexerFunc) error {
indexFunc := func(objRaw interface{}) ([]string, error) {
// TODO(directxman12): check if this is the correct type?
obj, isObj := objRaw.(runtime.Object)
if !isObj {
return nil, fmt.Errorf("object of type %T is not an Object", objRaw)
}
meta, err := apimeta.Accessor(obj)
if err != nil {
return nil, err
}
ns := meta.GetNamespace()
rawVals := extractor(obj)
var vals []string
if ns == "" {
// if we're not doubling the keys for the namespaced case, just re-use what was returned to us
vals = rawVals
} else {
// if we need to add non-namespaced versions too, double the length
vals = make([]string, len(rawVals)*2)
}
for i, rawVal := range rawVals {
// save a namespaced variant, so that we can ask
// "what are all the object matching a given index *in a given namespace*"
vals[i] = internal.KeyToNamespacedKey(ns, rawVal)
if ns != "" {
// if we have a namespace, also inject a special index key for listing
// regardless of the object namespace
vals[i+len(rawVals)] = internal.KeyToNamespacedKey("", rawVal)
}
}
return vals, nil
}
return indexer.AddIndexers(cache.Indexers{internal.FieldIndexName(field): indexFunc})
}

View File

@ -0,0 +1,141 @@
/*
Copyright 2018 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 informertest
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
toolscache "k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllertest"
)
var _ cache.Cache = &FakeInformers{}
// FakeInformers is a fake implementation of Informers
type FakeInformers struct {
InformersByGVK map[schema.GroupVersionKind]toolscache.SharedIndexInformer
Scheme *runtime.Scheme
Error error
Synced *bool
}
// GetInformerForKind implements Informers
func (c *FakeInformers) GetInformerForKind(gvk schema.GroupVersionKind) (toolscache.SharedIndexInformer, error) {
if c.Scheme == nil {
c.Scheme = scheme.Scheme
}
obj, err := c.Scheme.New(gvk)
if err != nil {
return nil, err
}
return c.informerFor(gvk, obj)
}
// FakeInformerForKind implements Informers
func (c *FakeInformers) FakeInformerForKind(gvk schema.GroupVersionKind) (*controllertest.FakeInformer, error) {
if c.Scheme == nil {
c.Scheme = scheme.Scheme
}
obj, err := c.Scheme.New(gvk)
if err != nil {
return nil, err
}
i, err := c.informerFor(gvk, obj)
if err != nil {
return nil, err
}
return i.(*controllertest.FakeInformer), nil
}
// GetInformer implements Informers
func (c *FakeInformers) GetInformer(obj runtime.Object) (toolscache.SharedIndexInformer, error) {
if c.Scheme == nil {
c.Scheme = scheme.Scheme
}
gvks, _, err := c.Scheme.ObjectKinds(obj)
if err != nil {
return nil, err
}
gvk := gvks[0]
return c.informerFor(gvk, obj)
}
// WaitForCacheSync implements Informers
func (c *FakeInformers) WaitForCacheSync(stop <-chan struct{}) bool {
if c.Synced == nil {
return true
}
return *c.Synced
}
// FakeInformerFor implements Informers
func (c *FakeInformers) FakeInformerFor(obj runtime.Object) (*controllertest.FakeInformer, error) {
if c.Scheme == nil {
c.Scheme = scheme.Scheme
}
gvks, _, err := c.Scheme.ObjectKinds(obj)
if err != nil {
return nil, err
}
gvk := gvks[0]
i, err := c.informerFor(gvk, obj)
if err != nil {
return nil, err
}
return i.(*controllertest.FakeInformer), nil
}
func (c *FakeInformers) informerFor(gvk schema.GroupVersionKind, _ runtime.Object) (toolscache.SharedIndexInformer, error) {
if c.Error != nil {
return nil, c.Error
}
if c.InformersByGVK == nil {
c.InformersByGVK = map[schema.GroupVersionKind]toolscache.SharedIndexInformer{}
}
informer, ok := c.InformersByGVK[gvk]
if ok {
return informer, nil
}
c.InformersByGVK[gvk] = &controllertest.FakeInformer{}
return c.InformersByGVK[gvk], nil
}
// Start implements Informers
func (c *FakeInformers) Start(stopCh <-chan struct{}) error {
return c.Error
}
// IndexField implements Cache
func (c *FakeInformers) IndexField(obj runtime.Object, field string, extractValue client.IndexerFunc) error {
return nil
}
// Get implements Cache
func (c *FakeInformers) Get(ctx context.Context, key client.ObjectKey, obj runtime.Object) error {
return nil
}
// List implements Cache
func (c *FakeInformers) List(ctx context.Context, opts *client.ListOptions, list runtime.Object) error {
return nil
}

View File

@ -0,0 +1,186 @@
/*
Copyright 2018 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 internal
import (
"context"
"fmt"
"reflect"
"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/client"
)
// CacheReader is a CacheReader
var _ client.Reader = &CacheReader{}
// CacheReader wraps a cache.Index to implement the client.CacheReader interface for a single type
type CacheReader struct {
// indexer is the underlying indexer wrapped by this cache.
indexer cache.Indexer
// groupVersionKind is the group-version-kind of the resource.
groupVersionKind schema.GroupVersionKind
}
// 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 runtime.Object) error {
storeKey := objectKeyToStoreKey(key)
// Lookup the object from the indexer cache
obj, exists, err := c.indexer.GetByKey(storeKey)
if err != nil {
return err
}
// Not found, return an error
if !exists {
// Resource gets transformed into Kind in the error anyway, so this is fine
return errors.NewNotFound(schema.GroupResource{
Group: c.groupVersionKind.Group,
Resource: c.groupVersionKind.Kind,
}, key.Name)
}
// Verify the result is a runtime.Object
if _, isObj := obj.(runtime.Object); !isObj {
// This should never happen
return fmt.Errorf("cache contained %T, which is not an Object", obj)
}
// deep copy to avoid mutating cache
// TODO(directxman12): revisit the decision to always deepcopy
obj = obj.(runtime.Object).DeepCopyObject()
// Copy the value of the item in the cache to the returned value
// TODO(directxman12): this is a terrible hack, pls fix (we should have deepcopyinto)
outVal := reflect.ValueOf(out)
objVal := reflect.ValueOf(obj)
if !objVal.Type().AssignableTo(outVal.Type()) {
return fmt.Errorf("cache had type %s, but %s was asked for", objVal.Type(), outVal.Type())
}
reflect.Indirect(outVal).Set(reflect.Indirect(objVal))
return nil
}
// List lists items out of the indexer and writes them to out
func (c *CacheReader) List(ctx context.Context, opts *client.ListOptions, out runtime.Object) error {
var objs []interface{}
var err error
if opts != nil && opts.FieldSelector != nil {
// TODO(directxman12): support more complicated field selectors by
// combining multiple indicies, GetIndexers, etc
field, val, requiresExact := requiresExactMatch(opts.FieldSelector)
if !requiresExact {
return fmt.Errorf("non-exact field matches are not supported by the cache")
}
// list all objects by the field selector. If this is namespaced and we have one, ask for the
// namespaced index key. Otherwise, ask for the non-namespaced variant by using the fake "all namespaces"
// namespace.
objs, err = c.indexer.ByIndex(FieldIndexName(field), KeyToNamespacedKey(opts.Namespace, val))
} else if opts != nil && opts.Namespace != "" {
objs, err = c.indexer.ByIndex(cache.NamespaceIndex, opts.Namespace)
} else {
objs = c.indexer.List()
}
if err != nil {
return err
}
var labelSel labels.Selector
if opts != nil && opts.LabelSelector != nil {
labelSel = opts.LabelSelector
}
outItems, err := c.getListItems(objs, labelSel)
if err != nil {
return err
}
return apimeta.SetList(out, outItems)
}
func (c *CacheReader) getListItems(objs []interface{}, labelSel labels.Selector) ([]runtime.Object, error) {
outItems := make([]runtime.Object, 0, len(objs))
for _, item := range objs {
obj, isObj := item.(runtime.Object)
if !isObj {
return nil, fmt.Errorf("cache contained %T, which is not an Object", obj)
}
meta, err := apimeta.Accessor(obj)
if err != nil {
return nil, err
}
if labelSel != nil {
lbls := labels.Set(meta.GetLabels())
if !labelSel.Matches(lbls) {
continue
}
}
outItems = append(outItems, obj.DeepCopyObject())
}
return outItems, nil
}
// objectKeyToStorageKey converts an object key to store key.
// It's akin to MetaNamespaceKeyFunc. It's separate from
// String to allow keeping the key format easily in sync with
// MetaNamespaceKeyFunc.
func objectKeyToStoreKey(k client.ObjectKey) string {
if k.Namespace == "" {
return k.Name
}
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 {
return "field:" + field
}
// noNamespaceNamespace is used as the "namespace" when we want to list across all namespaces
const allNamespacesNamespace = "__all_namespaces"
// KeyToNamespacedKey prefixes the given index key with a namespace
// for use in field selector indexes.
func KeyToNamespacedKey(ns string, baseKey string) string {
if ns != "" {
return ns + "/" + baseKey
}
return allNamespacesNamespace + "/" + baseKey
}

View File

@ -0,0 +1,96 @@
/*
Copyright 2018 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 internal
import (
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
)
// InformersMap create and caches Informers for (runtime.Object, schema.GroupVersionKind) pairs.
// It uses a standard parameter codec constructed based on the given generated Scheme.
type InformersMap struct {
// we abstract over the details of structured vs unstructured with the specificInformerMaps
structured *specificInformersMap
unstructured *specificInformersMap
// Scheme maps runtime.Objects to GroupVersionKinds
Scheme *runtime.Scheme
}
// NewInformersMap creates a new InformersMap that can create informers for
// both structured and unstructured objects.
func NewInformersMap(config *rest.Config,
scheme *runtime.Scheme,
mapper meta.RESTMapper,
resync time.Duration,
namespace string) *InformersMap {
return &InformersMap{
structured: newStructuredInformersMap(config, scheme, mapper, resync, namespace),
unstructured: newUnstructuredInformersMap(config, scheme, mapper, resync, namespace),
Scheme: scheme,
}
}
// Start calls Run on each of the informers and sets started to true. Blocks on the stop channel.
func (m *InformersMap) Start(stop <-chan struct{}) error {
go m.structured.Start(stop)
go m.unstructured.Start(stop)
<-stop
return nil
}
// WaitForCacheSync waits until all the caches have been synced.
func (m *InformersMap) WaitForCacheSync(stop <-chan struct{}) bool {
syncedFuncs := append([]cache.InformerSynced(nil), m.structured.HasSyncedFuncs()...)
syncedFuncs = append(syncedFuncs, m.unstructured.HasSyncedFuncs()...)
return cache.WaitForCacheSync(stop, syncedFuncs...)
}
// Get will create a new Informer and add it to the map of InformersMap if none exists. Returns
// the Informer from the map.
func (m *InformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) {
_, isUnstructured := obj.(*unstructured.Unstructured)
_, isUnstructuredList := obj.(*unstructured.UnstructuredList)
isUnstructured = isUnstructured || isUnstructuredList
if isUnstructured {
return m.unstructured.Get(gvk, obj)
}
return m.structured.Get(gvk, obj)
}
// newStructuredInformersMap creates a new InformersMap for structured objects.
func newStructuredInformersMap(config *rest.Config, scheme *runtime.Scheme, mapper meta.RESTMapper, resync time.Duration, namespace string) *specificInformersMap {
return newSpecificInformersMap(config, scheme, mapper, resync, namespace, createStructuredListWatch)
}
// newUnstructuredInformersMap creates a new InformersMap for unstructured objects.
func newUnstructuredInformersMap(config *rest.Config, scheme *runtime.Scheme, mapper meta.RESTMapper, resync time.Duration, namespace string) *specificInformersMap {
return newSpecificInformersMap(config, scheme, mapper, resync, namespace, createUnstructuredListWatch)
}

View File

@ -0,0 +1,281 @@
/*
Copyright 2018 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 internal
import (
"fmt"
"sync"
"time"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)
// clientListWatcherFunc knows how to create a ListWatcher
type createListWatcherFunc func(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error)
// newSpecificInformersMap returns a new specificInformersMap (like
// the generical InformersMap, except that it doesn't implement WaitForCacheSync).
func newSpecificInformersMap(config *rest.Config,
scheme *runtime.Scheme,
mapper meta.RESTMapper,
resync time.Duration,
namespace string,
createListWatcher createListWatcherFunc) *specificInformersMap {
ip := &specificInformersMap{
config: config,
Scheme: scheme,
mapper: mapper,
informersByGVK: make(map[schema.GroupVersionKind]*MapEntry),
codecs: serializer.NewCodecFactory(scheme),
paramCodec: runtime.NewParameterCodec(scheme),
resync: resync,
createListWatcher: createListWatcher,
namespace: namespace,
}
return ip
}
// MapEntry contains the cached data for an Informer
type MapEntry struct {
// Informer is the cached informer
Informer cache.SharedIndexInformer
// CacheReader wraps Informer and implements the CacheReader interface for a single type
Reader CacheReader
}
// specificInformersMap create and caches Informers for (runtime.Object, schema.GroupVersionKind) pairs.
// It uses a standard parameter codec constructed based on the given generated Scheme.
type specificInformersMap struct {
// Scheme maps runtime.Objects to GroupVersionKinds
Scheme *runtime.Scheme
// config is used to talk to the apiserver
config *rest.Config
// mapper maps GroupVersionKinds to Resources
mapper meta.RESTMapper
// informersByGVK is the cache of informers keyed by groupVersionKind
informersByGVK map[schema.GroupVersionKind]*MapEntry
// codecs is used to create a new REST client
codecs serializer.CodecFactory
// paramCodec is used by list and watch
paramCodec runtime.ParameterCodec
// stop is the stop channel to stop informers
stop <-chan struct{}
// resync is the frequency the informers are resynced
resync time.Duration
// mu guards access to the map
mu sync.RWMutex
// start is true if the informers have been started
started bool
// createClient knows how to create a client and a list object,
// and allows for abstracting over the particulars of structured vs
// unstructured objects.
createListWatcher createListWatcherFunc
// namespace is the namespace that all ListWatches are restricted to
// default or empty string means all namespaces
namespace string
}
// Start calls Run on each of the informers and sets started to true. Blocks on the stop channel.
// It doesn't return start because it can't return an error, and it's not a runnable directly.
func (ip *specificInformersMap) Start(stop <-chan struct{}) {
func() {
ip.mu.Lock()
defer ip.mu.Unlock()
// Set the stop channel so it can be passed to informers that are added later
ip.stop = stop
// Start each informer
for _, informer := range ip.informersByGVK {
go informer.Informer.Run(stop)
}
// Set started to true so we immediately start any informers added later.
ip.started = true
}()
<-stop
}
// HasSyncedFuncs returns all the HasSynced functions for the informers in this map.
func (ip *specificInformersMap) HasSyncedFuncs() []cache.InformerSynced {
ip.mu.RLock()
defer ip.mu.RUnlock()
syncedFuncs := make([]cache.InformerSynced, 0, len(ip.informersByGVK))
for _, informer := range ip.informersByGVK {
syncedFuncs = append(syncedFuncs, informer.Informer.HasSynced)
}
return syncedFuncs
}
// Get will create a new Informer and add it to the map of specificInformersMap if none exists. Returns
// the Informer from the map.
func (ip *specificInformersMap) Get(gvk schema.GroupVersionKind, obj runtime.Object) (*MapEntry, error) {
// Return the informer if it is found
i, ok := func() (*MapEntry, bool) {
ip.mu.RLock()
defer ip.mu.RUnlock()
i, ok := ip.informersByGVK[gvk]
return i, ok
}()
if ok {
return i, nil
}
// Do the mutex part in its own function so we can use defer without blocking pieces that don't
// need to be locked
var sync bool
i, err := func() (*MapEntry, error) {
ip.mu.Lock()
defer ip.mu.Unlock()
// Check the cache to see if we already have an Informer. If we do, return the Informer.
// This is for the case where 2 routines tried to get the informer when it wasn't in the map
// so neither returned early, but the first one created it.
var ok bool
i, ok := ip.informersByGVK[gvk]
if ok {
return i, nil
}
// Create a NewSharedIndexInformer and add it to the map.
var lw *cache.ListWatch
lw, err := ip.createListWatcher(gvk, ip)
if err != nil {
return nil, err
}
ni := cache.NewSharedIndexInformer(lw, obj, ip.resync, cache.Indexers{
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
})
i = &MapEntry{
Informer: ni,
Reader: CacheReader{indexer: ni.GetIndexer(), groupVersionKind: gvk},
}
ip.informersByGVK[gvk] = i
// Start the Informer if need by
// TODO(seans): write thorough tests and document what happens here - can you add indexers?
// can you add eventhandlers?
if ip.started {
sync = true
go i.Informer.Run(ip.stop)
}
return i, nil
}()
if err != nil {
return nil, err
}
if sync {
// Wait for it to sync before returning the Informer so that folks don't read from a stale cache.
if !cache.WaitForCacheSync(ip.stop, i.Informer.HasSynced) {
return nil, fmt.Errorf("failed waiting for %T Informer to sync", obj)
}
}
return i, err
}
// newListWatch returns a new ListWatch object that can be used to create a SharedIndexInformer.
func createStructuredListWatch(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error) {
// Kubernetes APIs work against Resources, not GroupVersionKinds. Map the
// groupVersionKind to the Resource API we will use.
mapping, err := ip.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return nil, err
}
client, err := apiutil.RESTClientForGVK(gvk, ip.config, ip.codecs)
if err != nil {
return nil, err
}
listGVK := gvk.GroupVersion().WithKind(gvk.Kind + "List")
listObj, err := ip.Scheme.New(listGVK)
if err != nil {
return nil, err
}
// Create a new ListWatch for the obj
return &cache.ListWatch{
ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
res := listObj.DeepCopyObject()
isNamespaceScoped := ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot
err := client.Get().NamespaceIfScoped(ip.namespace, isNamespaceScoped).Resource(mapping.Resource.Resource).VersionedParams(&opts, ip.paramCodec).Do().Into(res)
return res, err
},
// Setup the watch function
WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
// Watch needs to be set to true separately
opts.Watch = true
isNamespaceScoped := ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot
return client.Get().NamespaceIfScoped(ip.namespace, isNamespaceScoped).Resource(mapping.Resource.Resource).VersionedParams(&opts, ip.paramCodec).Watch()
},
}, nil
}
func createUnstructuredListWatch(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error) {
// Kubernetes APIs work against Resources, not GroupVersionKinds. Map the
// groupVersionKind to the Resource API we will use.
mapping, err := ip.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return nil, err
}
dynamicClient, err := dynamic.NewForConfig(ip.config)
if err != nil {
return nil, err
}
// Create a new ListWatch for the obj
return &cache.ListWatch{
ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
if ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot {
return dynamicClient.Resource(mapping.Resource).Namespace(ip.namespace).List(opts)
}
return dynamicClient.Resource(mapping.Resource).List(opts)
},
// Setup the watch function
WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
// Watch needs to be set to true separately
opts.Watch = true
if ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot {
return dynamicClient.Resource(mapping.Resource).Namespace(ip.namespace).Watch(opts)
}
return dynamicClient.Resource(mapping.Resource).Watch(opts)
},
}, nil
}

View File

@ -0,0 +1,88 @@
/*
Copyright 2018 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"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
)
// NewDiscoveryRESTMapper constructs a new RESTMapper based on discovery
// information fetched by a new client with the given config.
func NewDiscoveryRESTMapper(c *rest.Config) (meta.RESTMapper, error) {
// Get a mapper
dc := discovery.NewDiscoveryClientForConfigOrDie(c)
gr, err := restmapper.GetAPIGroupResources(dc)
if err != nil {
return nil, err
}
return restmapper.NewDiscoveryRESTMapper(gr), nil
}
// GVKForObject finds the GroupVersionKind associated with the given object, if there is only a single such GVK.
func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
gvks, isUnversioned, err := scheme.ObjectKinds(obj)
if err != nil {
return schema.GroupVersionKind{}, err
}
if isUnversioned {
return schema.GroupVersionKind{}, fmt.Errorf("cannot create a new informer for the unversioned type %T", obj)
}
if len(gvks) < 1 {
return schema.GroupVersionKind{}, fmt.Errorf("no group-version-kinds associated with type %T", obj)
}
if len(gvks) > 1 {
// this should only trigger for things like metav1.XYZ --
// normal versioned types should be fine
return schema.GroupVersionKind{}, fmt.Errorf(
"multiple group-version-kinds associated with type %T, refusing to guess at one", obj)
}
return gvks[0], nil
}
// RESTClientForGVK constructs a new rest.Interface capable of accessing the resource associated
// with the given GroupVersionKind.
func RESTClientForGVK(gvk schema.GroupVersionKind, baseConfig *rest.Config, codecs serializer.CodecFactory) (rest.Interface, error) {
cfg := createRestConfig(gvk, baseConfig)
cfg.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs}
return rest.RESTClientFor(cfg)
}
//createRestConfig copies the base config and updates needed fields for a new rest config
func createRestConfig(gvk schema.GroupVersionKind, baseConfig *rest.Config) *rest.Config {
gv := gvk.GroupVersion()
cfg := rest.CopyConfig(baseConfig)
cfg.GroupVersion = &gv
if gvk.Group == "" {
cfg.APIPath = "/api"
} else {
cfg.APIPath = "/apis"
}
if cfg.UserAgent == "" {
cfg.UserAgent = rest.DefaultKubernetesUserAgent()
}
return cfg
}

View File

@ -0,0 +1,162 @@
/*
Copyright 2018 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 client
import (
"context"
"fmt"
"reflect"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)
// Options are creation options for a Client
type Options struct {
// Scheme, if provided, will be used to map go structs to GroupVersionKinds
Scheme *runtime.Scheme
// Mapper, if provided, will be used to map GroupVersionKinds to Resources
Mapper meta.RESTMapper
}
// New returns a new Client using the provided config and Options.
func New(config *rest.Config, options Options) (Client, error) {
if config == nil {
return nil, fmt.Errorf("must provide non-nil rest.Config to client.New")
}
// Init a scheme if none provided
if options.Scheme == nil {
options.Scheme = scheme.Scheme
}
// Init a Mapper if none provided
if options.Mapper == nil {
var err error
options.Mapper, err = apiutil.NewDiscoveryRESTMapper(config)
if err != nil {
return nil, err
}
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
return nil, err
}
c := &client{
typedClient: typedClient{
cache: clientCache{
config: config,
scheme: options.Scheme,
mapper: options.Mapper,
codecs: serializer.NewCodecFactory(options.Scheme),
resourceByType: make(map[reflect.Type]*resourceMeta),
},
paramCodec: runtime.NewParameterCodec(options.Scheme),
},
unstructuredClient: unstructuredClient{
client: dynamicClient,
restMapper: options.Mapper,
},
}
return c, nil
}
var _ Client = &client{}
// 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.
type client struct {
typedClient typedClient
unstructuredClient unstructuredClient
}
// Create implements client.Client
func (c *client) Create(ctx context.Context, obj runtime.Object) error {
_, ok := obj.(*unstructured.Unstructured)
if ok {
return c.unstructuredClient.Create(ctx, obj)
}
return c.typedClient.Create(ctx, obj)
}
// Update implements client.Client
func (c *client) Update(ctx context.Context, obj runtime.Object) error {
_, ok := obj.(*unstructured.Unstructured)
if ok {
return c.unstructuredClient.Update(ctx, obj)
}
return c.typedClient.Update(ctx, obj)
}
// Delete implements client.Client
func (c *client) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error {
_, ok := obj.(*unstructured.Unstructured)
if ok {
return c.unstructuredClient.Delete(ctx, obj, opts...)
}
return c.typedClient.Delete(ctx, obj, opts...)
}
// Get implements client.Client
func (c *client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
_, ok := obj.(*unstructured.Unstructured)
if ok {
return c.unstructuredClient.Get(ctx, key, obj)
}
return c.typedClient.Get(ctx, key, obj)
}
// List implements client.Client
func (c *client) List(ctx context.Context, opts *ListOptions, obj runtime.Object) error {
_, ok := obj.(*unstructured.UnstructuredList)
if ok {
return c.unstructuredClient.List(ctx, opts, obj)
}
return c.typedClient.List(ctx, opts, obj)
}
// Status implements client.StatusClient
func (c *client) Status() StatusWriter {
return &statusWriter{client: c}
}
// statusWriter is client.StatusWriter that writes status subresource
type statusWriter struct {
client *client
}
// ensure statusWriter implements client.StatusWriter
var _ StatusWriter = &statusWriter{}
// Update implements client.StatusWriter
func (sw *statusWriter) Update(ctx context.Context, obj runtime.Object) error {
_, ok := obj.(*unstructured.Unstructured)
if ok {
return sw.client.unstructuredClient.UpdateStatus(ctx, obj)
}
return sw.client.typedClient.UpdateStatus(ctx, obj)
}

View File

@ -0,0 +1,145 @@
/*
Copyright 2018 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 client
import (
"reflect"
"strings"
"sync"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)
// clientCache creates and caches rest clients and metadata for Kubernetes types
type clientCache struct {
// config is the rest.Config to talk to an apiserver
config *rest.Config
// scheme maps go structs to GroupVersionKinds
scheme *runtime.Scheme
// mapper maps GroupVersionKinds to Resources
mapper meta.RESTMapper
// codecs are used to create a REST client for a gvk
codecs serializer.CodecFactory
// resourceByType caches type metadata
resourceByType map[reflect.Type]*resourceMeta
mu sync.RWMutex
}
// newResource maps obj to a Kubernetes Resource and constructs a client for that Resource.
// If the object is a list, the resource represents the item's type instead.
func (c *clientCache) newResource(obj runtime.Object) (*resourceMeta, error) {
gvk, err := apiutil.GVKForObject(obj, c.scheme)
if err != nil {
return nil, err
}
if strings.HasSuffix(gvk.Kind, "List") && meta.IsListType(obj) {
// if this was a list, treat it as a request for the item's resource
gvk.Kind = gvk.Kind[:len(gvk.Kind)-4]
}
client, err := apiutil.RESTClientForGVK(gvk, c.config, c.codecs)
if err != nil {
return nil, err
}
mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return nil, err
}
return &resourceMeta{Interface: client, mapping: mapping, gvk: gvk}, nil
}
// getResource returns the resource meta information for the given type of object.
// If the object is a list, the resource represents the item's type instead.
func (c *clientCache) getResource(obj runtime.Object) (*resourceMeta, error) {
typ := reflect.TypeOf(obj)
// It's better to do creation work twice than to not let multiple
// people make requests at once
c.mu.RLock()
r, known := c.resourceByType[typ]
c.mu.RUnlock()
if known {
return r, nil
}
// Initialize a new Client
c.mu.Lock()
defer c.mu.Unlock()
r, err := c.newResource(obj)
if err != nil {
return nil, err
}
c.resourceByType[typ] = r
return r, err
}
// getObjMeta returns objMeta containing both type and object metadata and state
func (c *clientCache) getObjMeta(obj runtime.Object) (*objMeta, error) {
r, err := c.getResource(obj)
if err != nil {
return nil, err
}
m, err := meta.Accessor(obj)
if err != nil {
return nil, err
}
return &objMeta{resourceMeta: r, Object: m}, err
}
// resourceMeta caches state for a Kubernetes type.
type resourceMeta struct {
// client is the rest client used to talk to the apiserver
rest.Interface
// gvk is the GroupVersionKind of the resourceMeta
gvk schema.GroupVersionKind
// mapping is the rest mapping
mapping *meta.RESTMapping
}
// isNamespaced returns true if the type is namespaced
func (r *resourceMeta) isNamespaced() bool {
if r.mapping.Scope.Name() == meta.RESTScopeNameRoot {
return false
}
return true
}
// resource returns the resource name of the type
func (r *resourceMeta) resource() string {
return r.mapping.Resource.Resource
}
// objMeta stores type and object information about a Kubernetes type
type objMeta struct {
// resourceMeta contains type information for the object
*resourceMeta
// Object contains meta data for the object instance
v1.Object
}

View File

@ -0,0 +1,57 @@
/*
Copyright 2018 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 client_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestSource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{envtest.NewlineReporter{}})
}
var testenv *envtest.Environment
var cfg *rest.Config
var clientset *kubernetes.Clientset
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
testenv = &envtest.Environment{}
var err error
cfg, err = testenv.Start()
Expect(err).NotTo(HaveOccurred())
clientset, err = kubernetes.NewForConfig(cfg)
Expect(err).NotTo(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
testenv.Stop()
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,94 @@
/*
Copyright 2017 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 config
import (
"flag"
"fmt"
"os"
"os/user"
"path/filepath"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
var (
kubeconfig, masterURL string
log = logf.KBLog.WithName("client").WithName("config")
)
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.")
flag.StringVar(&masterURL, "master", "",
"The address of the Kubernetes API server. Overrides any value in kubeconfig. "+
"Only required if out-of-cluster.")
}
// GetConfig 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.
//
// Config precedence
//
// * --kubeconfig flag pointing at a file
//
// * KUBECONFIG environment variable pointing at a file
//
// * In-cluster config if running in cluster
//
// * $HOME/.kube/config if exists
func GetConfig() (*rest.Config, error) {
// If a flag is specified with the config location, use that
if len(kubeconfig) > 0 {
return clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
}
// If an env variable is specified with the config locaiton, use that
if len(os.Getenv("KUBECONFIG")) > 0 {
return clientcmd.BuildConfigFromFlags(masterURL, os.Getenv("KUBECONFIG"))
}
// If no explicit location, try the in-cluster config
if c, err := rest.InClusterConfig(); err == nil {
return c, nil
}
// If no in-cluster config, try the default location in the user's home directory
if usr, err := user.Current(); err == nil {
if c, err := clientcmd.BuildConfigFromFlags(
"", filepath.Join(usr.HomeDir, ".kube", "config")); err == nil {
return c, nil
}
}
return nil, fmt.Errorf("could not locate a kubeconfig")
}
// 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.
//
// Will log an error and exit if there is an error creating the rest.Config.
func GetConfigOrDie() *rest.Config {
config, err := GetConfig()
if err != nil {
log.Error(err, "unable to get kubeconfig")
}
return config
}

View File

@ -0,0 +1,18 @@
/*
Copyright 2017 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 config contains libraries for initializing rest configs for talking to the Kubernetes API
package config

View File

@ -0,0 +1,154 @@
/*
Copyright 2018 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 fake
import (
"context"
"encoding/json"
"os"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/testing"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
var (
log = logf.KBLog.WithName("fake-client")
)
type fakeClient struct {
tracker testing.ObjectTracker
}
var _ client.Client = &fakeClient{}
// NewFakeClient creates a new fake client for testing.
// You can choose to initialize it with a slice of runtime.Object.
func NewFakeClient(initObjs ...runtime.Object) client.Client {
tracker := testing.NewObjectTracker(scheme.Scheme, scheme.Codecs.UniversalDecoder())
for _, obj := range initObjs {
err := tracker.Add(obj)
if err != nil {
log.Error(err, "failed to add object", "object", obj)
os.Exit(1)
return nil
}
}
return &fakeClient{
tracker: tracker,
}
}
func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj runtime.Object) error {
gvr, err := getGVRFromObject(obj)
if err != nil {
return err
}
o, err := c.tracker.Get(gvr, key.Namespace, key.Name)
if err != nil {
return err
}
j, err := json.Marshal(o)
if err != nil {
return err
}
decoder := scheme.Codecs.UniversalDecoder()
_, _, err = decoder.Decode(j, nil, obj)
return err
}
func (c *fakeClient) List(ctx context.Context, opts *client.ListOptions, list runtime.Object) error {
gvk := opts.Raw.TypeMeta.GroupVersionKind()
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
o, err := c.tracker.List(gvr, gvk, opts.Namespace)
if err != nil {
return err
}
j, err := json.Marshal(o)
if err != nil {
return err
}
decoder := scheme.Codecs.UniversalDecoder()
_, _, err = decoder.Decode(j, nil, list)
return err
}
func (c *fakeClient) Create(ctx context.Context, obj runtime.Object) error {
gvr, err := getGVRFromObject(obj)
if err != nil {
return err
}
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}
return c.tracker.Create(gvr, obj, accessor.GetNamespace())
}
func (c *fakeClient) Delete(ctx context.Context, obj runtime.Object, opts ...client.DeleteOptionFunc) error {
gvr, err := getGVRFromObject(obj)
if err != nil {
return err
}
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}
//TODO: implement propagation
return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
}
func (c *fakeClient) Update(ctx context.Context, obj runtime.Object) error {
gvr, err := getGVRFromObject(obj)
if err != nil {
return err
}
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
}
func (c *fakeClient) Status() client.StatusWriter {
return &fakeStatusWriter{client: c}
}
func getGVRFromObject(obj runtime.Object) (schema.GroupVersionResource, error) {
gvk, err := apiutil.GVKForObject(obj, scheme.Scheme)
if err != nil {
return schema.GroupVersionResource{}, err
}
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
return gvr, nil
}
type fakeStatusWriter struct {
client *fakeClient
}
func (sw *fakeStatusWriter) Update(ctx context.Context, obj runtime.Object) 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)
}

View File

@ -0,0 +1,37 @@
/*
Copyright 2018 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 fake
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestSource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{envtest.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
close(done)
}, 60)

View File

@ -0,0 +1,156 @@
/*
Copyright 2018 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 fake
import (
"encoding/json"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var _ = Describe("Fake client", func() {
var dep *appsv1.Deployment
var cm *corev1.ConfigMap
var cl client.Client
BeforeEach(func(done Done) {
dep = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Namespace: "ns1",
},
}
cm = &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "ns2",
},
Data: map[string]string{
"test-key": "test-value",
},
}
cl = NewFakeClient(dep, cm)
close(done)
})
It("should be able to Get", func() {
By("Getting a deployment")
namespacedName := types.NamespacedName{
Name: "test-deployment",
Namespace: "ns1",
}
obj := &appsv1.Deployment{}
err := cl.Get(nil, namespacedName, obj)
Expect(err).To(BeNil())
Expect(obj).To(Equal(dep))
})
It("should be able to List", func() {
By("Listing all deployments in a namespace")
list := &metav1.List{}
err := cl.List(nil, &client.ListOptions{
Namespace: "ns1",
Raw: &metav1.ListOptions{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
}, list)
Expect(err).To(BeNil())
Expect(list.Items).To(HaveLen(1))
j, err := json.Marshal(dep)
Expect(err).To(BeNil())
expectedDep := runtime.RawExtension{Raw: j}
Expect(list.Items).To(ConsistOf(expectedDep))
})
It("should be able to Create", func() {
By("Creating a new configmap")
newcm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "new-test-cm",
Namespace: "ns2",
},
}
err := cl.Create(nil, newcm)
Expect(err).To(BeNil())
By("Getting the new configmap")
namespacedName := types.NamespacedName{
Name: "new-test-cm",
Namespace: "ns2",
}
obj := &corev1.ConfigMap{}
err = cl.Get(nil, namespacedName, obj)
Expect(err).To(BeNil())
Expect(obj).To(Equal(newcm))
})
It("should be able to Update", func() {
By("Updating a new configmap")
newcm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "ns2",
},
Data: map[string]string{
"test-key": "new-value",
},
}
err := cl.Update(nil, newcm)
Expect(err).To(BeNil())
By("Getting the new configmap")
namespacedName := types.NamespacedName{
Name: "test-cm",
Namespace: "ns2",
}
obj := &corev1.ConfigMap{}
err = cl.Get(nil, namespacedName, obj)
Expect(err).To(BeNil())
Expect(obj).To(Equal(newcm))
})
It("should be able to Delete", func() {
By("Deleting a deployment")
err := cl.Delete(nil, dep)
Expect(err).To(BeNil())
By("Listing all deployments in the namespace")
list := &metav1.List{}
err = cl.List(nil, &client.ListOptions{
Namespace: "ns1",
Raw: &metav1.ListOptions{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
}, list)
Expect(err).To(BeNil())
Expect(list.Items).To(HaveLen(0))
})
})

View File

@ -0,0 +1,27 @@
/*
Copyright 2018 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 fake provides a fake client for testing.
An fake client is backed by its simple object store indexed by GroupVersionResource.
You can create a fake client with optional objects.
client := NewFakeClient(initObjs...) // initObjs is a slice of runtime.Object
You can invoke the methods defined in the Client interface.
*/
package fake

View File

@ -0,0 +1,292 @@
/*
Copyright 2018 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 client
import (
"context"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
)
// ObjectKey identifies a Kubernetes Object.
type ObjectKey = types.NamespacedName
// ObjectKeyFromObject returns the ObjectKey given a runtime.Object
func ObjectKeyFromObject(obj runtime.Object) (ObjectKey, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return ObjectKey{}, err
}
return ObjectKey{Namespace: accessor.GetNamespace(), Name: accessor.GetName()}, nil
}
// TODO(directxman12): is there a sane way to deal with get/delete options?
// Reader knows how to read and list Kubernetes objects.
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 runtime.Object) 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
// result returned from the server.
List(ctx context.Context, opts *ListOptions, list runtime.Object) error
}
// Writer knows how to create, delete, and update Kubernetes objects.
type Writer interface {
// Create saves the object obj in the Kubernetes cluster.
Create(ctx context.Context, obj runtime.Object) error
// Delete deletes the given obj from Kubernetes cluster.
Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error
// Update updates the given obj in the Kubernetes cluster. obj must be a
// struct pointer so that obj can be updated with the content returned by the Server.
Update(ctx context.Context, obj runtime.Object) error
}
// StatusClient knows how to create a client which can update status subresource
// for kubernetes objects.
type StatusClient interface {
Status() StatusWriter
}
// StatusWriter knows how to update status subresource of a Kubernetes object.
type StatusWriter interface {
// 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 runtime.Object) error
}
// Client knows how to perform CRUD operations on Kubernetes objects.
type Client interface {
Reader
Writer
StatusClient
}
// IndexerFunc knows how to take an object and turn it into a series
// of (non-namespaced) keys for that object.
type IndexerFunc func(runtime.Object) []string
// FieldIndexer knows how to index over a particular "field" such that it
// can later be used by a field selector.
type FieldIndexer interface {
// IndexFields adds an index with the given field name on the given object type
// by using the given function to extract the value for that field. If you want
// compatibility with the Kubernetes API server, only return one key, and only use
// fields that the API server supports. Otherwise, you can return multiple keys,
// and "equality" in the field selector means that at least one key matches the value.
IndexField(obj runtime.Object, field string, extractValue IndexerFunc) error
}
// DeleteOptions contains options for delete requests. It's generally a subset
// of metav1.DeleteOptions.
type DeleteOptions struct {
// GracePeriodSeconds is the duration in seconds before the object should be
// deleted. Value must be non-negative integer. The value zero indicates
// delete immediately. If this value is nil, the default grace period for the
// specified type will be used.
GracePeriodSeconds *int64
// Preconditions must be fulfilled before a deletion is carried out. If not
// possible, a 409 Conflict status will be returned.
Preconditions *metav1.Preconditions
// PropagationPolicy determined whether and how garbage collection will be
// performed. Either this field or OrphanDependents may be set, but not both.
// The default policy is decided by the existing finalizer set in the
// metadata.finalizers and the resource-specific default policy.
// Acceptable values are: 'Orphan' - orphan the dependents; 'Background' -
// allow the garbage collector to delete the dependents in the background;
// 'Foreground' - a cascading policy that deletes all dependents in the
// foreground.
PropagationPolicy *metav1.DeletionPropagation
// Raw represents raw DeleteOptions, as passed to the API server.
Raw *metav1.DeleteOptions
}
// AsDeleteOptions returns these options as a metav1.DeleteOptions.
// This may mutate the Raw field.
func (o *DeleteOptions) AsDeleteOptions() *metav1.DeleteOptions {
if o == nil {
return &metav1.DeleteOptions{}
}
if o.Raw == nil {
o.Raw = &metav1.DeleteOptions{}
}
o.Raw.GracePeriodSeconds = o.GracePeriodSeconds
o.Raw.Preconditions = o.Preconditions
o.Raw.PropagationPolicy = o.PropagationPolicy
return o.Raw
}
// ApplyOptions executes the given DeleteOptionFuncs and returns the mutated
// DeleteOptions.
func (o *DeleteOptions) ApplyOptions(optFuncs []DeleteOptionFunc) *DeleteOptions {
for _, optFunc := range optFuncs {
optFunc(o)
}
return o
}
// DeleteOptionFunc is a function that mutates a DeleteOptions struct. It implements
// the functional options pattern. See
// https://github.com/tmrts/go-patterns/blob/master/idiom/functional-options.md.
type DeleteOptionFunc func(*DeleteOptions)
// GracePeriodSeconds is a functional option that sets the GracePeriodSeconds
// field of a DeleteOptions struct.
func GracePeriodSeconds(gp int64) DeleteOptionFunc {
return func(opts *DeleteOptions) {
opts.GracePeriodSeconds = &gp
}
}
// Preconditions is a functional option that sets the Preconditions field of a
// DeleteOptions struct.
func Preconditions(p *metav1.Preconditions) DeleteOptionFunc {
return func(opts *DeleteOptions) {
opts.Preconditions = p
}
}
// PropagationPolicy is a functional option that sets the PropagationPolicy
// field of a DeleteOptions struct.
func PropagationPolicy(p metav1.DeletionPropagation) DeleteOptionFunc {
return func(opts *DeleteOptions) {
opts.PropagationPolicy = &p
}
}
// ListOptions contains options for limitting or filtering results.
// It's generally a subset of metav1.ListOptions, with support for
// pre-parsed selectors (since generally, selectors will be executed
// against the cache).
type ListOptions struct {
// LabelSelector filters results by label. Use SetLabelSelector to
// set from raw string form.
LabelSelector labels.Selector
// FieldSelector filters results by a particular field. In order
// to use this with cache-based implementations, restrict usage to
// a single field-value pair that's been added to the indexers.
FieldSelector fields.Selector
// Namespace represents the namespace to list for, or empty for
// non-namespaced objects, or to list across all namespaces.
Namespace string
// 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 and FieldSelector fields are ignored.
Raw *metav1.ListOptions
}
// SetLabelSelector sets this the label selector of these options
// from a string form of the selector.
func (o *ListOptions) SetLabelSelector(selRaw string) error {
sel, err := labels.Parse(selRaw)
if err != nil {
return err
}
o.LabelSelector = sel
return nil
}
// SetFieldSelector sets this the label selector of these options
// from a string form of the selector.
func (o *ListOptions) SetFieldSelector(selRaw string) error {
sel, err := fields.ParseSelector(selRaw)
if err != nil {
return err
}
o.FieldSelector = sel
return nil
}
// AsListOptions returns these options as a flattened metav1.ListOptions.
// This may mutate the Raw field.
func (o *ListOptions) AsListOptions() *metav1.ListOptions {
if o == nil {
return &metav1.ListOptions{}
}
if o.Raw == nil {
o.Raw = &metav1.ListOptions{}
}
if o.LabelSelector != nil {
o.Raw.LabelSelector = o.LabelSelector.String()
}
if o.FieldSelector != nil {
o.Raw.FieldSelector = o.FieldSelector.String()
}
return o.Raw
}
// MatchingLabels is a convenience function that sets the label selector
// to match the given labels, and then returns the options.
// It mutates the list options.
func (o *ListOptions) MatchingLabels(lbls map[string]string) *ListOptions {
sel := labels.SelectorFromSet(lbls)
o.LabelSelector = sel
return o
}
// MatchingField is a convenience function that sets the field selector
// to match the given field, and then returns the options.
// It mutates the list options.
func (o *ListOptions) MatchingField(name, val string) *ListOptions {
sel := fields.SelectorFromSet(fields.Set{name: val})
o.FieldSelector = sel
return o
}
// InNamespace is a convenience function that sets the namespace,
// and then returns the options. It mutates the list options.
func (o *ListOptions) InNamespace(ns string) *ListOptions {
o.Namespace = ns
return o
}
// MatchingLabels is a convenience function that constructs list options
// to match the given labels.
func MatchingLabels(lbls map[string]string) *ListOptions {
return (&ListOptions{}).MatchingLabels(lbls)
}
// MatchingField is a convenience function that constructs list options
// to match the given field.
func MatchingField(name, val string) *ListOptions {
return (&ListOptions{}).MatchingField(name, val)
}
// InNamespace is a convenience function that constructs list
// options to list in the given namespace.
func InNamespace(ns string) *ListOptions {
return (&ListOptions{}).InNamespace(ns)
}

View File

@ -0,0 +1,59 @@
/*
Copyright 2018 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 client
import (
"context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
// DelegatingClient forms an interface Client by composing separate
// reader, writer and statusclient interfaces. This way, you can have an Client that
// reads from a cache and writes to the API server.
type DelegatingClient struct {
Reader
Writer
StatusClient
}
// DelegatingReader forms a interface Reader that will cause Get and List
// requests for unstructured types to use the ClientReader while
// requests for any other type of object with use the CacheReader.
type DelegatingReader struct {
CacheReader Reader
ClientReader Reader
}
// Get retrieves an obj for a given object key from the Kubernetes Cluster.
func (d *DelegatingReader) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
_, isUnstructured := obj.(*unstructured.Unstructured)
if isUnstructured {
return d.ClientReader.Get(ctx, key, obj)
}
return d.CacheReader.Get(ctx, key, obj)
}
// List retrieves list of objects for a given namespace and list options.
func (d *DelegatingReader) List(ctx context.Context, opts *ListOptions, list runtime.Object) error {
_, isUnstructured := list.(*unstructured.UnstructuredList)
if isUnstructured {
return d.ClientReader.List(ctx, opts, list)
}
return d.CacheReader.List(ctx, opts, list)
}

View File

@ -0,0 +1,133 @@
/*
Copyright 2018 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 client
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
)
// 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.
type typedClient struct {
cache clientCache
paramCodec runtime.ParameterCodec
}
// Create implements client.Client
func (c *typedClient) Create(ctx context.Context, obj runtime.Object) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
}
return o.Post().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Body(obj).
Context(ctx).
Do().
Into(obj)
}
// Update implements client.Client
func (c *typedClient) Update(ctx context.Context, obj runtime.Object) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
}
return o.Put().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
Body(obj).
Context(ctx).
Do().
Into(obj)
}
// Delete implements client.Client
func (c *typedClient) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
}
deleteOpts := DeleteOptions{}
return o.Delete().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
Body(deleteOpts.ApplyOptions(opts).AsDeleteOptions()).
Context(ctx).
Do().
Error()
}
// Get implements client.Client
func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
r, err := c.cache.getResource(obj)
if err != nil {
return err
}
return r.Get().
NamespaceIfScoped(key.Namespace, r.isNamespaced()).
Resource(r.resource()).
Context(ctx).
Name(key.Name).Do().Into(obj)
}
// List implements client.Client
func (c *typedClient) List(ctx context.Context, opts *ListOptions, obj runtime.Object) error {
r, err := c.cache.getResource(obj)
if err != nil {
return err
}
namespace := ""
if opts != nil {
namespace = opts.Namespace
}
return r.Get().
NamespaceIfScoped(namespace, r.isNamespaced()).
Resource(r.resource()).
Body(obj).
VersionedParams(opts.AsListOptions(), c.paramCodec).
Context(ctx).
Do().
Into(obj)
}
// UpdateStatus used by StatusWriter to write status.
func (c *typedClient) UpdateStatus(ctx context.Context, obj runtime.Object) error {
o, err := c.cache.getObjMeta(obj)
if err != nil {
return err
}
// TODO(droot): examine the returned error and check if it error needs to be
// 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
return o.Put().
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
Resource(o.resource()).
Name(o.GetName()).
SubResource("status").
Body(obj).
Context(ctx).
Do().
Into(obj)
}

View File

@ -0,0 +1,162 @@
/*
Copyright 2018 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 client
import (
"context"
"fmt"
"strings"
"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/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)
// 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.
type unstructuredClient struct {
client dynamic.Interface
restMapper meta.RESTMapper
}
// Create implements client.Client
func (uc *unstructuredClient) Create(_ context.Context, obj runtime.Object) error {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace())
if err != nil {
return err
}
i, err := r.Create(u)
if err != nil {
return err
}
u.Object = i.Object
return nil
}
// Update implements client.Client
func (uc *unstructuredClient) Update(_ context.Context, obj runtime.Object) error {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace())
if err != nil {
return err
}
i, err := r.Update(u)
if err != nil {
return err
}
u.Object = i.Object
return nil
}
// Delete implements client.Client
func (uc *unstructuredClient) Delete(_ context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace())
if err != nil {
return err
}
deleteOpts := DeleteOptions{}
err = r.Delete(u.GetName(), deleteOpts.ApplyOptions(opts).AsDeleteOptions())
return err
}
// Get implements client.Client
func (uc *unstructuredClient) Get(_ context.Context, key ObjectKey, obj runtime.Object) error {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
r, err := uc.getResourceInterface(u.GroupVersionKind(), key.Namespace)
if err != nil {
return err
}
i, err := r.Get(key.Name, metav1.GetOptions{})
if err != nil {
return err
}
u.Object = i.Object
return nil
}
// List implements client.Client
func (uc *unstructuredClient) List(_ context.Context, opts *ListOptions, obj runtime.Object) error {
u, ok := obj.(*unstructured.UnstructuredList)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
gvk := u.GroupVersionKind()
if strings.HasSuffix(gvk.Kind, "List") {
gvk.Kind = gvk.Kind[:len(gvk.Kind)-4]
}
namespace := ""
if opts != nil {
namespace = opts.Namespace
}
r, err := uc.getResourceInterface(gvk, namespace)
if err != nil {
return err
}
i, err := r.List(*opts.AsListOptions())
if err != nil {
return err
}
u.Items = i.Items
u.Object = i.Object
return nil
}
func (uc *unstructuredClient) UpdateStatus(_ context.Context, obj runtime.Object) error {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unstructured client did not understand object: %T", obj)
}
r, err := uc.getResourceInterface(u.GroupVersionKind(), u.GetNamespace())
if err != nil {
return err
}
i, err := r.UpdateStatus(u)
if err != nil {
return err
}
u.Object = i.Object
return nil
}
func (uc *unstructuredClient) getResourceInterface(gvk schema.GroupVersionKind, ns string) (dynamic.ResourceInterface, error) {
mapping, err := uc.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return nil, err
}
if mapping.Scope.Name() == meta.RESTScopeNameRoot {
return uc.client.Resource(mapping.Resource), nil
}
return uc.client.Resource(mapping.Resource).Namespace(ns), nil
}

View File

@ -0,0 +1,94 @@
/*
Copyright 2018 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 controller
import (
"fmt"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/internal/controller"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
// Options are the arguments for creating a new Controller
type Options struct {
// MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1.
MaxConcurrentReconciles int
// Reconciler reconciles an object
Reconciler reconcile.Reconciler
}
// Controller implements a Kubernetes API. A Controller manages a work queue fed reconcile.Requests
// from source.Sources. Work is performed through the reconcile.Reconciler for each enqueued item.
// Work typically is reads and writes Kubernetes objects to make the system state match the state specified
// in the object Spec.
type Controller interface {
// Reconciler is called to Reconciler an object by Namespace/Name
reconcile.Reconciler
// Watch takes events provided by a Source and uses the EventHandler to enqueue reconcile.Requests in
// response to the events.
//
// Watch may be provided one or more Predicates to filter events before they are given to the EventHandler.
// Events will be passed to the EventHandler iff all provided Predicates evaluate to true.
Watch(src source.Source, eventhandler handler.EventHandler, predicates ...predicate.Predicate) error
// Start starts the controller. Start blocks until stop is closed or a controller has an error starting.
Start(stop <-chan struct{}) error
}
// New returns a new Controller registered with the Manager. The Manager will ensure that shared Caches have
// been synced before the Controller is Started.
func New(name string, mgr manager.Manager, options Options) (Controller, error) {
if options.Reconciler == nil {
return nil, fmt.Errorf("must specify Reconciler")
}
if len(name) == 0 {
return nil, fmt.Errorf("must specify Name for Controller")
}
if options.MaxConcurrentReconciles <= 0 {
options.MaxConcurrentReconciles = 1
}
// Inject dependencies into Reconciler
if err := mgr.SetFields(options.Reconciler); err != nil {
return nil, err
}
// Create controller with dependencies set
c := &controller.Controller{
Do: options.Reconciler,
Cache: mgr.GetCache(),
Config: mgr.GetConfig(),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Recorder: mgr.GetRecorder(name),
Queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), name),
MaxConcurrentReconciles: options.MaxConcurrentReconciles,
Name: name,
}
// Add the controller as a Manager components
return c, mgr.Add(c)
}

View File

@ -0,0 +1,162 @@
/*
Copyright 2018 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 controller_test
import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
var _ = Describe("controller", func() {
var reconciled chan reconcile.Request
var stop chan struct{}
BeforeEach(func() {
stop = make(chan struct{})
reconciled = make(chan reconcile.Request)
Expect(cfg).NotTo(BeNil())
})
AfterEach(func() {
close(stop)
})
Describe("controller", func() {
// TODO(directxman12): write a whole suite of controller-client interaction tests
It("should reconcile", func(done Done) {
By("Creating the Manager")
cm, err := manager.New(cfg, manager.Options{})
Expect(err).NotTo(HaveOccurred())
By("Creating the Controller")
instance, err := controller.New("foo-controller", cm, controller.Options{
Reconciler: reconcile.Func(
func(request reconcile.Request) (reconcile.Result, error) {
reconciled <- request
return reconcile.Result{}, nil
}),
})
Expect(err).NotTo(HaveOccurred())
By("Watching Resources")
err = instance.Watch(&source.Kind{Type: &appsv1.ReplicaSet{}}, &handler.EnqueueRequestForOwner{
OwnerType: &appsv1.Deployment{},
})
Expect(err).NotTo(HaveOccurred())
err = instance.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{})
Expect(err).NotTo(HaveOccurred())
By("Starting the Manager")
go func() {
defer GinkgoRecover()
Expect(cm.Start(stop)).NotTo(HaveOccurred())
}()
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "deployment-name"},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
},
},
}
expectedReconcileRequest := reconcile.Request{NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "deployment-name",
}}
By("Invoking Reconciling for Create")
deployment, err = clientset.AppsV1().Deployments("default").Create(deployment)
Expect(err).NotTo(HaveOccurred())
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
By("Invoking Reconciling for Update")
newDeployment := deployment.DeepCopy()
newDeployment.Labels = map[string]string{"foo": "bar"}
newDeployment, err = clientset.AppsV1().Deployments("default").Update(newDeployment)
Expect(err).NotTo(HaveOccurred())
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
By("Invoking Reconciling for an OwnedObject when it is created")
replicaset := &appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs-name",
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(deployment, schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}),
},
},
Spec: appsv1.ReplicaSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Template: deployment.Spec.Template,
},
}
replicaset, err = clientset.AppsV1().ReplicaSets("default").Create(replicaset)
Expect(err).NotTo(HaveOccurred())
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
By("Invoking Reconciling for an OwnedObject when it is updated")
newReplicaset := replicaset.DeepCopy()
newReplicaset.Labels = map[string]string{"foo": "bar"}
newReplicaset, err = clientset.AppsV1().ReplicaSets("default").Update(newReplicaset)
Expect(err).NotTo(HaveOccurred())
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
By("Invoking Reconciling for an OwnedObject when it is deleted")
err = clientset.AppsV1().ReplicaSets("default").Delete(replicaset.Name, &metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
By("Invoking Reconciling for Delete")
err = clientset.AppsV1().Deployments("default").
Delete("deployment-name", &metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
close(done)
}, 5)
})
})

View File

@ -0,0 +1,56 @@
/*
Copyright 2018 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 controller_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestSource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{envtest.NewlineReporter{}})
}
var testenv *envtest.Environment
var cfg *rest.Config
var clientset *kubernetes.Clientset
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
testenv = &envtest.Environment{}
var err error
cfg, err = testenv.Start()
Expect(err).NotTo(HaveOccurred())
clientset, err = kubernetes.NewForConfig(cfg)
Expect(err).NotTo(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
testenv.Stop()
})

View File

@ -0,0 +1,92 @@
/*
Copyright 2018 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 controller_test
import (
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
)
var _ = Describe("controller.Controller", func() {
var stop chan struct{}
rec := reconcile.Func(func(reconcile.Request) (reconcile.Result, error) {
return reconcile.Result{}, nil
})
BeforeEach(func() {
stop = make(chan struct{})
})
AfterEach(func() {
close(stop)
})
Describe("New", func() {
It("should return an error if Name is not Specified", func(done Done) {
m, err := manager.New(cfg, manager.Options{})
Expect(err).NotTo(HaveOccurred())
c, err := controller.New("", m, controller.Options{Reconciler: rec})
Expect(c).To(BeNil())
Expect(err.Error()).To(ContainSubstring("must specify Name for Controller"))
close(done)
})
It("should return an error if Reconciler is not Specified", func(done Done) {
m, err := manager.New(cfg, manager.Options{})
Expect(err).NotTo(HaveOccurred())
c, err := controller.New("foo", m, controller.Options{})
Expect(c).To(BeNil())
Expect(err.Error()).To(ContainSubstring("must specify Reconciler"))
close(done)
})
It("NewController should return an error if injecting Reconciler fails", func(done Done) {
m, err := manager.New(cfg, manager.Options{})
Expect(err).NotTo(HaveOccurred())
c, err := controller.New("foo", m, controller.Options{Reconciler: &failRec{}})
Expect(c).To(BeNil())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("expected error"))
close(done)
})
})
})
var _ reconcile.Reconciler = &failRec{}
var _ inject.Client = &failRec{}
type failRec struct{}
func (*failRec) Reconcile(reconcile.Request) (reconcile.Result, error) {
return reconcile.Result{}, nil
}
func (*failRec) InjectClient(client.Client) error {
return fmt.Errorf("expected error")
}

View File

@ -0,0 +1,18 @@
/*
Copyright 2017 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 controllertest contains fake informers for testing controllers
package controllertest

View File

@ -0,0 +1,62 @@
/*
Copyright 2018 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 controllertest
import (
"time"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/util/workqueue"
)
var _ runtime.Object = &ErrorType{}
// ErrorType implements runtime.Object but isn't registered in any scheme and should cause errors in tests as a result.
type ErrorType struct{}
// GetObjectKind implements runtime.Object
func (ErrorType) GetObjectKind() schema.ObjectKind { return nil }
// DeepCopyObject implements runtime.Object
func (ErrorType) DeepCopyObject() runtime.Object { return nil }
var _ workqueue.RateLimitingInterface = Queue{}
// Queue implements a RateLimiting queue as a non-ratelimited queue for testing.
// This helps testing by having functions that use a RateLimiting queue synchronously add items to the queue.
type Queue struct {
workqueue.Interface
}
// AddAfter implements RateLimitingInterface.
func (q Queue) AddAfter(item interface{}, duration time.Duration) {
q.Add(item)
}
// AddRateLimited implements RateLimitingInterface. TODO(community): Implement this.
func (q Queue) AddRateLimited(item interface{}) {
q.Add(item)
}
// Forget implements RateLimitingInterface. TODO(community): Implement this.
func (q Queue) Forget(item interface{}) {}
// NumRequeues implements RateLimitingInterface. TODO(community): Implement this.
func (q Queue) NumRequeues(item interface{}) int {
return 0
}

View File

@ -0,0 +1,108 @@
/*
Copyright 2017 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 controllertest
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/cache"
)
var _ cache.SharedIndexInformer = &FakeInformer{}
// FakeInformer provides fake Informer functionality for testing
type FakeInformer struct {
// Synced is returned by the HasSynced functions to implement the Informer interface
Synced bool
// RunCount is incremented each time RunInformersAndControllers is called
RunCount int
handlers []cache.ResourceEventHandler
}
// AddIndexers does nothing. TODO(community): Implement this.
func (f *FakeInformer) AddIndexers(indexers cache.Indexers) error {
return nil
}
// GetIndexer does nothing. TODO(community): Implement this.
func (f *FakeInformer) GetIndexer() cache.Indexer {
return nil
}
// Informer returns the fake Informer.
func (f *FakeInformer) Informer() cache.SharedIndexInformer {
return f
}
// HasSynced implements the Informer interface. Returns f.Synced
func (f *FakeInformer) HasSynced() bool {
return f.Synced
}
// AddEventHandler implements the Informer interface. Adds an EventHandler to the fake Informers.
func (f *FakeInformer) AddEventHandler(handler cache.ResourceEventHandler) {
f.handlers = append(f.handlers, handler)
}
// Run implements the Informer interface. Increments f.RunCount
func (f *FakeInformer) Run(<-chan struct{}) {
f.RunCount++
}
// Add fakes an Add event for obj
func (f *FakeInformer) Add(obj metav1.Object) {
for _, h := range f.handlers {
h.OnAdd(obj)
}
}
// Update fakes an Update event for obj
func (f *FakeInformer) Update(oldObj, newObj metav1.Object) {
for _, h := range f.handlers {
h.OnUpdate(oldObj, newObj)
}
}
// Delete fakes an Delete event for obj
func (f *FakeInformer) Delete(obj metav1.Object) {
for _, h := range f.handlers {
h.OnDelete(obj)
}
}
// AddEventHandlerWithResyncPeriod does nothing. TODO(community): Implement this.
func (f *FakeInformer) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, resyncPeriod time.Duration) {
}
// GetStore does nothing. TODO(community): Implement this.
func (f *FakeInformer) GetStore() cache.Store {
return nil
}
// GetController does nothing. TODO(community): Implement this.
func (f *FakeInformer) GetController() cache.Controller {
return nil
}
// LastSyncResourceVersion does nothing. TODO(community): Implement this.
func (f *FakeInformer) LastSyncResourceVersion() string {
return ""
}

View File

@ -0,0 +1,178 @@
/*
Copyright 2018 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 controllerutil
import (
"context"
"fmt"
"reflect"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)
// AlreadyOwnedError is an error returned if the object you are trying to assign
// a controller reference is already owned by another controller Object is the
// subject and Owner is the reference for the current owner
type AlreadyOwnedError struct {
Object v1.Object
Owner v1.OwnerReference
}
func (e *AlreadyOwnedError) Error() string {
return fmt.Sprintf("Object %s/%s is already owned by another %s controller %s", e.Object.GetNamespace(), e.Object.GetName(), e.Owner.Kind, e.Owner.Name)
}
func newAlreadyOwnedError(Object v1.Object, Owner v1.OwnerReference) *AlreadyOwnedError {
return &AlreadyOwnedError{
Object: Object,
Owner: Owner,
}
}
// SetControllerReference sets owner as a Controller OwnerReference on owned.
// This is used for garbage collection of the owned object and for
// reconciling the owner object on changes to owned (with a Watch + EnqueueRequestForOwner).
// Since only one OwnerReference can be a controller, it returns an error if
// there is another OwnerReference with Controller flag set.
func SetControllerReference(owner, object v1.Object, scheme *runtime.Scheme) error {
ro, ok := owner.(runtime.Object)
if !ok {
return fmt.Errorf("is not a %T a runtime.Object, cannot call SetControllerReference", owner)
}
gvk, err := apiutil.GVKForObject(ro, scheme)
if err != nil {
return err
}
// Create a new ref
ref := *v1.NewControllerRef(owner, schema.GroupVersionKind{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind})
existingRefs := object.GetOwnerReferences()
fi := -1
for i, r := range existingRefs {
if referSameObject(ref, r) {
fi = i
} else if r.Controller != nil && *r.Controller {
return newAlreadyOwnedError(object, r)
}
}
if fi == -1 {
existingRefs = append(existingRefs, ref)
} else {
existingRefs[fi] = ref
}
// Update owner references
object.SetOwnerReferences(existingRefs)
return nil
}
// Returns true if a and b point to the same object
func referSameObject(a, b v1.OwnerReference) bool {
aGV, err := schema.ParseGroupVersion(a.APIVersion)
if err != nil {
return false
}
bGV, err := schema.ParseGroupVersion(b.APIVersion)
if err != nil {
return false
}
return aGV == bGV && a.Kind == b.Kind && a.Name == b.Name
}
// OperationResult is the action result of a CreateOrUpdate call
type OperationResult string
const ( // They should complete the sentence "Deployment default/foo has been ..."
// OperationResultNone means that the resource has not been changed
OperationResultNone OperationResult = "unchanged"
// OperationResultCreated means that a new resource is created
OperationResultCreated OperationResult = "created"
// OperationResultUpdated means that an existing resource is updated
OperationResultUpdated OperationResult = "updated"
)
// CreateOrUpdate creates or updates the given object obj in the Kubernetes
// cluster. The object's desired state should be reconciled with the existing
// state using the passed in ReconcileFn. obj must be a struct pointer so that
// obj can be updated with the content returned by the Server.
//
// It returns the executed operation and an error.
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, f MutateFn) (OperationResult, error) {
// op is the operation we are going to attempt
op := OperationResultNone
// get the existing object meta
metaObj, ok := obj.(v1.Object)
if !ok {
return OperationResultNone, fmt.Errorf("%T does not implement metav1.Object interface", obj)
}
// retrieve the existing object
key := client.ObjectKey{
Name: metaObj.GetName(),
Namespace: metaObj.GetNamespace(),
}
err := c.Get(ctx, key, obj)
// reconcile the existing object
existing := obj.DeepCopyObject()
existingObjMeta := existing.(v1.Object)
existingObjMeta.SetName(metaObj.GetName())
existingObjMeta.SetNamespace(metaObj.GetNamespace())
if e := f(obj); e != nil {
return OperationResultNone, e
}
if metaObj.GetName() != existingObjMeta.GetName() {
return OperationResultNone, fmt.Errorf("ReconcileFn cannot mutate objects name")
}
if metaObj.GetNamespace() != existingObjMeta.GetNamespace() {
return OperationResultNone, fmt.Errorf("ReconcileFn cannot mutate objects namespace")
}
if errors.IsNotFound(err) {
err = c.Create(ctx, obj)
op = OperationResultCreated
} else if err == nil {
if reflect.DeepEqual(existing, obj) {
return OperationResultNone, nil
}
err = c.Update(ctx, obj)
op = OperationResultUpdated
} else {
return OperationResultNone, err
}
if err != nil {
op = OperationResultNone
}
return op, err
}
// MutateFn is a function which mutates the existing object into it's desired state.
type MutateFn func(existing runtime.Object) error

View File

@ -0,0 +1,53 @@
/*
Copyright 2018 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 controllerutil_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
func TestControllerutil(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controllerutil Suite")
}
var t *envtest.Environment
var cfg *rest.Config
var c client.Client
var _ = BeforeSuite(func() {
var err error
t = &envtest.Environment{}
cfg, err = t.Start()
Expect(err).NotTo(HaveOccurred())
c, err = client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
})
var _ = AfterSuite(func() {
t.Stop()
})

View File

@ -0,0 +1,275 @@
/*
Copyright 2018 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 controllerutil_test
import (
"context"
"fmt"
"math/rand"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
var _ = Describe("Controllerutil", func() {
Describe("SetControllerReference", func() {
It("should set the OwnerReference if it can find the group version kind", func() {
rs := &appsv1.ReplicaSet{}
dep := &extensionsv1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "foo", UID: "foo-uid"},
}
Expect(controllerutil.SetControllerReference(dep, rs, scheme.Scheme)).NotTo(HaveOccurred())
t := true
Expect(rs.OwnerReferences).To(ConsistOf(metav1.OwnerReference{
Name: "foo",
Kind: "Deployment",
APIVersion: "extensions/v1beta1",
UID: "foo-uid",
Controller: &t,
BlockOwnerDeletion: &t,
}))
})
It("should return an error if it can't find the group version kind of the owner", func() {
rs := &appsv1.ReplicaSet{}
dep := &extensionsv1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
}
Expect(controllerutil.SetControllerReference(dep, rs, runtime.NewScheme())).To(HaveOccurred())
})
It("should return an error if the owner isn't a runtime.Object", func() {
rs := &appsv1.ReplicaSet{}
Expect(controllerutil.SetControllerReference(&errMetaObj{}, rs, scheme.Scheme)).To(HaveOccurred())
})
It("should return an error if object is already owned by another controller", func() {
t := true
rsOwners := []metav1.OwnerReference{
metav1.OwnerReference{
Name: "bar",
Kind: "Deployment",
APIVersion: "extensions/v1beta1",
UID: "bar-uid",
Controller: &t,
BlockOwnerDeletion: &t,
},
}
rs := &appsv1.ReplicaSet{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default", OwnerReferences: rsOwners}}
dep := &extensionsv1beta1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default", UID: "foo-uid"}}
err := controllerutil.SetControllerReference(dep, rs, scheme.Scheme)
Expect(err).To(HaveOccurred())
Expect(err).To(BeAssignableToTypeOf(&controllerutil.AlreadyOwnedError{}))
})
It("should not duplicate existing owner reference", func() {
f := false
t := true
rsOwners := []metav1.OwnerReference{
metav1.OwnerReference{
Name: "foo",
Kind: "Deployment",
APIVersion: "extensions/v1beta1",
UID: "foo-uid",
Controller: &f,
BlockOwnerDeletion: &t,
},
}
rs := &appsv1.ReplicaSet{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default", OwnerReferences: rsOwners}}
dep := &extensionsv1beta1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default", UID: "foo-uid"}}
Expect(controllerutil.SetControllerReference(dep, rs, scheme.Scheme)).NotTo(HaveOccurred())
Expect(rs.OwnerReferences).To(ConsistOf(metav1.OwnerReference{
Name: "foo",
Kind: "Deployment",
APIVersion: "extensions/v1beta1",
UID: "foo-uid",
Controller: &t,
BlockOwnerDeletion: &t,
}))
})
})
Describe("CreateOrUpdate", func() {
var deploy *appsv1.Deployment
var deplSpec appsv1.DeploymentSpec
var deplKey types.NamespacedName
BeforeEach(func() {
deploy = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("deploy-%d", rand.Int31()),
Namespace: "default",
},
}
deplSpec = appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"foo": "bar",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
corev1.Container{
Name: "busybox",
Image: "busybox",
},
},
},
},
}
deplKey = types.NamespacedName{
Name: deploy.Name,
Namespace: deploy.Namespace,
}
})
It("creates a new object if one doesn't exists", func() {
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
By("returning OperationResultCreatedd")
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
By("returning no error")
Expect(err).NotTo(HaveOccurred())
By("actually having the deployment created")
fetched := &appsv1.Deployment{}
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
})
It("updates existing object", func() {
var scale int32 = 2
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
Expect(err).NotTo(HaveOccurred())
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentScaler(scale))
By("returning OperationResultUpdatedd")
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdated))
By("returning no error")
Expect(err).NotTo(HaveOccurred())
By("actually having the deployment scaled")
fetched := &appsv1.Deployment{}
Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed())
Expect(*fetched.Spec.Replicas).To(Equal(scale))
})
It("updates only changed objects", func() {
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
Expect(err).NotTo(HaveOccurred())
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentIdentity)
By("returning OperationResultNone")
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
By("returning no error")
Expect(err).NotTo(HaveOccurred())
})
It("errors when reconcile renames an object", func() {
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
Expect(err).NotTo(HaveOccurred())
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentRenamer)
By("returning OperationResultNone")
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
By("returning error")
Expect(err).To(HaveOccurred())
})
It("errors when object namespace changes", func() {
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentSpecr(deplSpec))
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated))
Expect(err).NotTo(HaveOccurred())
op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentNamespaceChanger)
By("returning OperationResultNone")
Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone))
By("returning error")
Expect(err).To(HaveOccurred())
})
})
})
var _ metav1.Object = &errMetaObj{}
type errMetaObj struct {
metav1.ObjectMeta
}
func deploymentSpecr(spec appsv1.DeploymentSpec) controllerutil.MutateFn {
return func(obj runtime.Object) error {
deploy := obj.(*appsv1.Deployment)
deploy.Spec = spec
return nil
}
}
var deploymentIdentity controllerutil.MutateFn = func(obj runtime.Object) error {
return nil
}
var deploymentRenamer controllerutil.MutateFn = func(obj runtime.Object) error {
deploy := obj.(*appsv1.Deployment)
deploy.Name = fmt.Sprintf("%s-1", deploy.Name)
return nil
}
var deploymentNamespaceChanger controllerutil.MutateFn = func(obj runtime.Object) error {
deploy := obj.(*appsv1.Deployment)
deploy.Namespace = fmt.Sprintf("%s-1", deploy.Namespace)
return nil
}
func deploymentScaler(replicas int32) controllerutil.MutateFn {
fn := func(obj runtime.Object) error {
deploy := obj.(*appsv1.Deployment)
deploy.Spec.Replicas = &replicas
return nil
}
return fn
}

View File

@ -0,0 +1,20 @@
/*
Copyright 2018 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 controllerutil contains utility functions for working with and implementing Controllers.
*/
package controllerutil

View File

@ -0,0 +1,78 @@
/*
Copyright 2018 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 controllerutil_test
import (
"context"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
var (
log = logf.Log.WithName("controllerutil-examples")
)
// This example creates or updates an existing deployment
func ExampleCreateOrUpdate() {
// c is client.Client
// Create or Update the deployment default/foo
deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deployment, func(existing runtime.Object) error {
deploy := existing.(*appsv1.Deployment)
// Deployment selector is immutable so we set this value only if
// a new object is going to be created
if deploy.ObjectMeta.CreationTimestamp.IsZero() {
deploy.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
}
}
// update the Deployment pod template
deploy.Spec.Template = corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"foo": "bar",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
corev1.Container{
Name: "busybox",
Image: "busybox",
},
},
},
}
return nil
})
if err != nil {
log.Error(err, "Deployment reconcile failed")
} else {
log.Info("Deployment successfully reconciled", "operation", op)
}
}

View File

@ -0,0 +1,25 @@
/*
Copyright 2018 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 controller provides types and functions for building Controllers. Controllers implement Kubernetes APIs.
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.
*/
package controller

View File

@ -0,0 +1,79 @@
/*
Copyright 2018 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 controller_test
import (
"os"
"k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var (
mgr manager.Manager
// NB: don't call SetLogger in init(), or else you'll mess up logging in the main suite.
log = logf.Log.WithName("controller-examples")
)
// This example creates a new Controller named "pod-controller" with a no-op reconcile function. The
// manager.Manager will be used to Start the Controller, and will provide it a shared Cache and Client.
func ExampleNew() {
_, err := controller.New("pod-controller", mgr, controller.Options{
Reconciler: reconcile.Func(func(o reconcile.Request) (reconcile.Result, error) {
// Your business logic to implement the API by creating, updating, deleting objects goes here.
return reconcile.Result{}, nil
}),
})
if err != nil {
log.Error(err, "unable to create pod-controller")
os.Exit(1)
}
}
// This example starts a new Controller named "pod-controller" to Watch Pods and call a no-op Reconciler.
func ExampleController() {
// mgr is a manager.Manager
// Create a new Controller that will call the provided Reconciler function in response
// to events.
c, err := controller.New("pod-controller", mgr, controller.Options{
Reconciler: reconcile.Func(func(o reconcile.Request) (reconcile.Result, error) {
// Your business logic to implement the API by creating, updating, deleting objects goes here.
return reconcile.Result{}, nil
}),
})
if err != nil {
log.Error(err, "unable to create pod-controller")
os.Exit(1)
}
// Watch for Pod create / update / delete events and call Reconcile
err = c.Watch(&source.Kind{Type: &v1.Pod{}}, &handler.EnqueueRequestForObject{})
if err != nil {
log.Error(err, "unable to watch pods")
os.Exit(1)
}
// Start the Controller through the manager.
mgr.Start(signals.SetupSignalHandler())
}

207
vendor/sigs.k8s.io/controller-runtime/pkg/doc.go generated vendored Normal file
View File

@ -0,0 +1,207 @@
/*
Copyright 2018 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 pkg provides libraries for building Controllers. Controllers implement Kubernetes APIs
and are foundational to building Operators, Workload APIs, Configuration APIs, Autoscalers, and more.
Client
Client provides a Read + Write client for reading and writing Kubernetes objects.
Cache
Cache provides a Read client for reading objects from a local cache.
A cache may register handlers to respond to events that update the cache.
Manager
Manager is required for creating a Controller and provides the Controller shared dependencies such as
clients, caches, schemes, etc. Controllers should be Started through the Manager by calling Manager.Start.
Controller
Controller implements a Kubernetes API by responding to events (object Create, Update, Delete) and ensuring that
the state specified in the Spec of the object matches the state of the system. This is called a Reconciler.
If they do not match, the Controller will create / update / delete objects as needed to make them match.
Controllers are implemented as worker queues that process reconcile.Requests (requests to Reconciler the
state for a specific object).
Unlike http handlers, Controllers DO NOT handle events directly, but enqueue Requests to eventually Reconciler
the object. This means the handling of multiple events may be batched together and the full state of the
system must be read for each Reconciler.
* Controllers require a Reconciler to be provided to perform the work pulled from the work queue.
* Controller require Watches to be configured to enqueue reconcile.Requests in response to events.
Webhook
Admission Webhooks are a mechanism for extending kubernetes APIs. Webhooks can be configured with target
event type (object Create, Update, Delete), the API server will send AdmissionRequests to them
when certain events happen. The webhooks may mutate and (or) validate the object embedded in
the AdmissionReview requests and send back the response to the API server.
There are 2 types of admission webhook: mutating and validating admission webhook.
Mutating webhook is used to mutate a core API object or a CRD instance before the API server admits it.
Validating webhook is used to validate if an object meets certain requirements.
* Admission Webhooks require Handler(s) to be provided to process the received AdmissionReview requests.
Reconciler
Reconciler is a function provided to a Controller that may be called at anytime with the Name and Namespace of an object.
When called, Reconciler will ensure that the state of the system matches what is specified in the object at the
time Reconciler is called.
Example: Reconciler invoked for a ReplicaSet object. The ReplicaSet specifies 5 replicas but only
3 Pods exist in the system. Reconciler creates 2 more Pods and sets their OwnerReference to point at the
ReplicaSet with controller=true.
* Reconciler contains all of the business logic of a Controller.
* Reconciler typically works on a single object type. - e.g. it will only reconcile ReplicaSets. For separate
types use separate Controllers. If you wish to trigger reconciles from other objects, you can provide
a mapping (e.g. owner references) that maps the object that triggers the reconcile to the object being reconciled.
* Reconciler is provided the Name / Namespace of the object to reconcile.
* Reconciler does not care about the event contents or event type responsible for triggering the Reconciler.
- e.g. it doesn't matter whether a ReplicaSet was created or updated, Reconciler will always compare the number of
Pods in the system against what is specified in the object at the time it is called.
Source
resource.Source is an argument to Controller.Watch that provides a stream of events.
Events typically come from watching Kubernetes APIs (e.g. Pod Create, Update, Delete).
Example: source.Kind uses the Kubernetes API Watch endpoint for a GroupVersionKind to provide
Create, Update, Delete events.
* Source provides a stream of events (e.g. object Create, Update, Delete) for Kubernetes objects typically
through the Watch API.
* Users SHOULD only use the provided Source implementations instead of implementing their own for nearly all cases.
EventHandler
handler.EventHandler is a argument to Controller.Watch that enqueues reconcile.Requests in response to events.
Example: a Pod Create event from a Source is provided to the eventhandler.EnqueueHandler, which enqueues a
reconcile.Request containing the name / Namespace of the Pod.
* EventHandlers handle events by enqueueing reconcile.Requests for one or more objects.
* EventHandlers MAY map an event for an object to a reconcile.Request for an object of the same type.
* EventHandlers MAY map an event for an object to a reconcile.Request for an object of a different type - e.g.
map a Pod event to a reconcile.Request for the owning ReplicaSet.
* EventHandlers MAY map an event for an object to multiple reconcile.Requests for objects of the same or a different
type - e.g. map a Node event to objects that respond to cluster resize events.
* Users SHOULD only use the provided EventHandler implementations instead of implementing their own for almost
all cases.
Predicate
predicate.Predicate is an optional argument to Controller.Watch that filters events. This allows common filters to be
reused and composed.
* Predicate takes an event and returns a bool (true to enqueue)
* Predicates are optional arguments
* Users SHOULD use the provided Predicate implementations, but MAY implement additional
Predicates e.g. generation changed, label selectors changed etc.
PodController Diagram
Source provides event:
* &source.KindSource{&v1.Pod{}} -> (Pod foo/bar Create Event)
EventHandler enqueues Request:
* &handler.EnqueueRequestForObject{} -> (reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}})
Reconciler is called with the Request:
* Reconciler(reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}})
Usage
The following example shows creating a new Controller program which Reconciles ReplicaSet objects in response
to Pod or ReplicaSet events. The Reconciler function simply adds a label to the ReplicaSet.
See the example/main.go for a usage example.
Controller Example
1. Watch ReplicaSet and Pods Sources
1.1 ReplicaSet -> handler.EnqueueRequestForObject - enqueue a Request with the ReplicaSet Namespace and Name.
1.2 Pod (created by ReplicaSet) -> handler.EnqueueRequestForOwnerHandler - enqueue a Request with the
Owning ReplicaSet Namespace and Name.
2. Reconciler ReplicaSet in response to an event
2.1 ReplicaSet object created -> Read ReplicaSet, try to read Pods -> if is missing create Pods.
2.2 Reconciler triggered by creation of Pods -> Read ReplicaSet and Pods, do nothing.
2.3 Reconciler triggered by deletion of Pods from some other actor -> Read ReplicaSet and Pods, create replacement Pods.
Watching and EventHandling
Controllers may Watch multiple Kinds of objects (e.g. Pods, ReplicaSets and Deployments), but they Reconciler
only a single Type. When one Type of object must be be updated in response to changes in another Type of object,
an EnqueueRequestFromMapFunc may be used to map events from one type to another. e.g. Respond to a cluster resize
event (add / delete Node) by re-reconciling all instances of some API.
A Deployment Controller might use an EnqueueRequestForObject and EnqueueRequestForOwner to:
* Watch for Deployment Events - enqueue the Namespace and Name of the Deployment.
* Watch for ReplicaSet Events - enqueue the Namespace and Name of the Deployment that created the ReplicaSet
(e.g the Owner)
Note: reconcile.Requests are deduplicated when they are enqueued. Many Pod Events for the same ReplicaSet
may trigger only 1 reconcile invocation as each Event results in the Handler trying to enqueue
the same reconcile.Request for the ReplicaSet.
Controller Writing Tips
Reconciler Runtime Complexity:
* It is better to write Controllers to perform an O(1) Reconciler N times (e.g. on N different objects) instead of
performing an O(N) Reconciler 1 time (e.g. on a single object which manages N other objects).
* Example: If you need to update all Services in response to a Node being added - Reconciler Services but Watch
Nodes (transformed to Service object name / Namespaces) instead of Reconciling Nodes and updating Services
Event Multiplexing:
* reconcile.Requests for the same Name / Namespace are batched and deduplicated when they are enqueued. This allows
Controllers to gracefully handle a high volume of events for a single object. Multiplexing multiple event Sources to
a single object Type will batch requests across events for different object types.
* Example: Pod events for a ReplicaSet are transformed to a ReplicaSet Name / Namespace, so the ReplicaSet
will be Reconciled only 1 time for multiple events from multiple Pods.
*/
package pkg

View File

@ -0,0 +1,221 @@
/*
Copyright 2018 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 envtest
import (
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/ghodss/yaml"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
)
// CRDInstallOptions are the options for installing CRDs
type CRDInstallOptions struct {
// Paths is the path to the directory containing CRDs
Paths []string
// CRDs is a list of CRDs to install
CRDs []*apiextensionsv1beta1.CustomResourceDefinition
// ErrorIfPathMissing will cause an error if a Path does not exist
ErrorIfPathMissing bool
// maxTime is the max time to wait
maxTime time.Duration
// pollInterval is the interval to check
pollInterval time.Duration
}
const defaultPollInterval = 100 * time.Millisecond
const 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) ([]*apiextensionsv1beta1.CustomResourceDefinition, error) {
defaultCRDOptions(&options)
// Read the CRD yamls into options.CRDs
if err := readCRDFiles(&options); err != nil {
return nil, err
}
// Create the CRDs in the apiserver
if err := CreateCRDs(config, options.CRDs); err != nil {
return options.CRDs, err
}
// Wait for the CRDs to appear as Resources in the apiserver
if err := WaitForCRDs(config, options.CRDs, options); err != nil {
return options.CRDs, err
}
return options.CRDs, nil
}
// readCRDFiles reads the directories of CRDs in options.Paths and adds the CRD structs to options.CRDs
func readCRDFiles(options *CRDInstallOptions) error {
if len(options.Paths) > 0 {
for _, path := range options.Paths {
if _, err := os.Stat(path); !options.ErrorIfPathMissing && os.IsNotExist(err) {
continue
}
new, err := readCRDs(path)
if err != nil {
return err
}
options.CRDs = append(options.CRDs, new...)
}
}
return nil
}
// defaultCRDOptions sets the default values for CRDs
func defaultCRDOptions(o *CRDInstallOptions) {
if o.maxTime == 0 {
o.maxTime = defaultMaxWait
}
if o.pollInterval == 0 {
o.pollInterval = defaultPollInterval
}
}
// WaitForCRDs waits for the CRDs to appear in discovery
func WaitForCRDs(config *rest.Config, crds []*apiextensionsv1beta1.CustomResourceDefinition, options CRDInstallOptions) error {
// Add each CRD to a map of GroupVersion to Resource
waitingFor := map[schema.GroupVersion]*sets.String{}
for _, crd := range crds {
gv := schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}
if _, found := waitingFor[gv]; !found {
// Initialize the set
waitingFor[gv] = &sets.String{}
}
// Add the Resource
waitingFor[gv].Insert(crd.Spec.Names.Plural)
}
// Poll until all resources are found in discovery
p := &poller{config: config, waitingFor: waitingFor}
return wait.PollImmediate(options.pollInterval, options.maxTime, p.poll)
}
// poller checks if all the resources have been found in discovery, and returns false if not
type poller struct {
// config is used to get discovery
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
}
// poll checks if all the resources have been found in discovery, and returns false if not
func (p *poller) poll() (done bool, err error) {
// Create a new clientset to avoid any client caching of discovery
cs, err := clientset.NewForConfig(p.config)
if err != nil {
return false, err
}
allFound := true
for gv, resources := range p.waitingFor {
// All resources found, do nothing
if resources.Len() == 0 {
delete(p.waitingFor, gv)
continue
}
// Get the Resources for this GroupVersion
// TODO: Maybe the controller-runtime client should be able to do this...
resourceList, err := cs.Discovery().ServerResourcesForGroupVersion(gv.Group + "/" + gv.Version)
if err != nil {
return false, nil
}
// Remove each found resource from the resources set that we are waiting for
for _, resource := range resourceList.APIResources {
resources.Delete(resource.Name)
}
// Still waiting on some resources in this group version
if resources.Len() != 0 {
allFound = false
}
}
return allFound, nil
}
// CreateCRDs creates the CRDs
func CreateCRDs(config *rest.Config, crds []*apiextensionsv1beta1.CustomResourceDefinition) error {
cs, err := clientset.NewForConfig(config)
if err != nil {
return err
}
// Create each CRD
for _, crd := range crds {
if _, err := cs.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd); err != nil {
return err
}
}
return nil
}
// readCRDs reads the CRDs from files and Unmarshals them into structs
func readCRDs(path string) ([]*apiextensionsv1beta1.CustomResourceDefinition, error) {
// Get the CRD files
var files []os.FileInfo
var err error
if files, err = ioutil.ReadDir(path); err != nil {
return nil, err
}
// White list the file extensions that may contain CRDs
crdExts := sets.NewString(".json", ".yaml", ".yml")
var crds []*apiextensionsv1beta1.CustomResourceDefinition
for _, file := range files {
// Only parse whitelisted file types
if !crdExts.Has(filepath.Ext(file.Name())) {
continue
}
// Unmarshal the file into a struct
b, err := ioutil.ReadFile(filepath.Join(path, file.Name()))
if err != nil {
return nil, err
}
crd := &apiextensionsv1beta1.CustomResourceDefinition{}
if err = yaml.Unmarshal(b, crd); err != nil {
return nil, err
}
// Check that it is actually a CRD
if crd.Spec.Names.Kind == "" || crd.Spec.Group == "" {
continue
}
crds = append(crds, crd)
}
return crds, nil
}

View File

@ -0,0 +1,18 @@
/*
Copyright 2017 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 envtest provides libraries for integration testing by starting a local control plane
package envtest

View File

@ -0,0 +1,47 @@
/*
Copyright 2018 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 envtest
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestSource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "EnvTest Suite", []Reporter{NewlineReporter{}})
}
var env *Environment
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
env = &Environment{}
_, err := env.Start()
Expect(err).NotTo(HaveOccurred())
close(done)
}, StartTimeout)
var _ = AfterSuite(func(done Done) {
Expect(env.Stop()).NotTo(HaveOccurred())
close(done)
}, StopTimeout)

View File

@ -0,0 +1,165 @@
/*
Copyright 2018 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 envtest
import (
"context"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var _ = Describe("Test", func() {
var crds []*v1beta1.CustomResourceDefinition
var err error
var s *runtime.Scheme
var c client.Client
// Initialize the client
BeforeEach(func(done Done) {
crds = []*v1beta1.CustomResourceDefinition{}
s = runtime.NewScheme()
err = v1beta1.AddToScheme(s)
Expect(err).NotTo(HaveOccurred())
c, err = client.New(env.Config, client.Options{Scheme: s})
Expect(err).NotTo(HaveOccurred())
close(done)
})
// Cleanup CRDs
AfterEach(func(done Done) {
for _, crd := range crds {
c.Delete(context.TODO(), crd)
}
close(done)
})
Describe("InstallCRDs", func() {
It("should install the CRDs into the cluster", func(done Done) {
crds, err = InstallCRDs(env.Config, CRDInstallOptions{
Paths: []string{"."},
})
Expect(err).NotTo(HaveOccurred())
// Expect to find the CRDs
crd := &v1beta1.CustomResourceDefinition{}
err = c.Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crd)
Expect(err).NotTo(HaveOccurred())
Expect(crd.Spec.Names.Kind).To(Equal("Foo"))
crd = &v1beta1.CustomResourceDefinition{}
err = c.Get(context.TODO(), types.NamespacedName{Name: "bazs.qux.example.com"}, crd)
Expect(err).NotTo(HaveOccurred())
Expect(crd.Spec.Names.Kind).To(Equal("Baz"))
err = WaitForCRDs(env.Config, []*v1beta1.CustomResourceDefinition{
{
Spec: v1beta1.CustomResourceDefinitionSpec{
Group: "qux.example.com",
Version: "v1beta1",
Names: v1beta1.CustomResourceDefinitionNames{
Plural: "bazs",
}},
},
{
Spec: v1beta1.CustomResourceDefinitionSpec{
Group: "bar.example.com",
Version: "v1beta1",
Names: v1beta1.CustomResourceDefinitionNames{
Plural: "foos",
}},
}},
CRDInstallOptions{maxTime: 50 * time.Millisecond, pollInterval: 15 * time.Millisecond},
)
Expect(err).NotTo(HaveOccurred())
close(done)
}, 5)
It("should not return an not error if the directory doesn't exist", func(done Done) {
crds, err = InstallCRDs(env.Config, CRDInstallOptions{Paths: []string{"fake"}})
Expect(err).NotTo(HaveOccurred())
close(done)
}, 5)
It("should return an error if the directory doesn't exist", func(done Done) {
crds, err = InstallCRDs(env.Config, CRDInstallOptions{Paths: []string{"fake"}, ErrorIfPathMissing: true})
Expect(err).To(HaveOccurred())
close(done)
}, 5)
It("should return an error if the resource group version isn't found", func(done Done) {
// Wait for a CRD where the Group and Version don't exist
err := WaitForCRDs(env.Config,
[]*v1beta1.CustomResourceDefinition{
{
Spec: v1beta1.CustomResourceDefinitionSpec{Names: v1beta1.CustomResourceDefinitionNames{
Plural: "notfound",
}},
},
},
CRDInstallOptions{maxTime: 50 * time.Millisecond, pollInterval: 15 * time.Millisecond},
)
Expect(err).To(HaveOccurred())
close(done)
}, 5)
It("should return an error if the resource isn't found in the group version", func(done Done) {
crds, err = InstallCRDs(env.Config, CRDInstallOptions{
Paths: []string{"."},
})
Expect(err).NotTo(HaveOccurred())
// Wait for a CRD that doesn't exist, but the Group and Version do
err = WaitForCRDs(env.Config, []*v1beta1.CustomResourceDefinition{
{
Spec: v1beta1.CustomResourceDefinitionSpec{
Group: "qux.example.com",
Version: "v1beta1",
Names: v1beta1.CustomResourceDefinitionNames{
Plural: "bazs",
}},
},
{
Spec: v1beta1.CustomResourceDefinitionSpec{
Group: "bar.example.com",
Version: "v1beta1",
Names: v1beta1.CustomResourceDefinitionNames{
Plural: "fake",
}},
}},
CRDInstallOptions{maxTime: 50 * time.Millisecond, pollInterval: 15 * time.Millisecond},
)
Expect(err).To(HaveOccurred())
close(done)
}, 5)
})
})

View File

@ -0,0 +1,11 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: foos.bar.example.com
spec:
group: bar.example.com
names:
kind: Foo
plural: foos
scope: Namespaced
version: "v1beta1"

View File

@ -0,0 +1,11 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: bazs.qux.example.com
spec:
group: qux.example.com
names:
kind: Baz
plural: bazs
scope: Namespaced
version: "v1beta1"

View File

@ -0,0 +1,11 @@
package envtest
import (
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
)
// 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
// It's re-exported here to avoid compatibility breakage/mass rewrites.
type NewlineReporter = printer.NewlineReporter

View File

@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9

View File

@ -0,0 +1,51 @@
/*
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
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

@ -0,0 +1,214 @@
/*
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 envtest
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/testing_frameworks/integration"
)
// Default binary path for test framework
const (
envKubeAPIServerBin = "TEST_ASSET_KUBE_APISERVER"
envEtcdBin = "TEST_ASSET_ETCD"
envKubectlBin = "TEST_ASSET_KUBECTL"
envKubebuilderPath = "KUBEBUILDER_ASSETS"
envStartTimeout = "KUBEBUILDER_CONTROLPLANE_START_TIMEOUT"
envStopTimeout = "KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT"
defaultKubebuilderPath = "/usr/local/kubebuilder/bin"
StartTimeout = 60
StopTimeout = 60
defaultKubebuilderControlPlaneStartTimeout = 20 * time.Second
defaultKubebuilderControlPlaneStopTimeout = 20 * time.Second
)
func defaultAssetPath(binary string) string {
assetPath := os.Getenv(envKubebuilderPath)
if assetPath == "" {
assetPath = defaultKubebuilderPath
}
return filepath.Join(assetPath, binary)
}
// APIServerDefaultArgs are flags necessary to bring up apiserver.
// TODO: create test framework interface to append flag to default flags.
var defaultKubeAPIServerFlags = []string{
"--etcd-servers={{ if .EtcdURL }}{{ .EtcdURL.String }}{{ end }}",
"--cert-dir={{ .CertDir }}",
"--insecure-port={{ if .URL }}{{ .URL.Port }}{{ end }}",
"--insecure-bind-address={{ if .URL }}{{ .URL.Hostname }}{{ end }}",
"--secure-port=0",
"--admission-control=AlwaysAdmit",
}
// Environment creates a Kubernetes test environment that will start / stop the Kubernetes control plane and
// install extension APIs
type Environment struct {
// ControlPlane is the ControlPlane including the apiserver and etcd
ControlPlane integration.ControlPlane
// Config can be used to talk to the apiserver
Config *rest.Config
// CRDs is a list of CRDs to install
CRDs []*apiextensionsv1beta1.CustomResourceDefinition
// CRDDirectoryPaths is a list of paths containing CRD yaml or json configs.
CRDDirectoryPaths []string
// UseExisting indicates that this environments should use an
// existing kubeconfig, instead of trying to stand up a new control plane.
// This is useful in cases that need aggregated API servers and the like.
UseExistingCluster bool
// ControlPlaneStartTimeout is the the maximum duration each controlplane component
// may take to start. It defaults to the KUBEBUILDER_CONTROLPLANE_START_TIMEOUT
// environment variable or 20 seconds if unspecified
ControlPlaneStartTimeout time.Duration
// ControlPlaneStopTimeout is the the maximum duration each controlplane component
// may take to stop. It defaults to the KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT
// environment variable or 20 seconds if unspecified
ControlPlaneStopTimeout time.Duration
}
// Stop stops a running server
func (te *Environment) Stop() error {
if te.UseExistingCluster {
return nil
}
return te.ControlPlane.Stop()
}
// Start starts a local Kubernetes server and updates te.ApiserverPort with the port it is listening on
func (te *Environment) Start() (*rest.Config, error) {
if te.UseExistingCluster {
if te.Config == nil {
// we want to allow people to pass in their own config, so
// only load a config if it hasn't already been set.
var err error
te.Config, err = config.GetConfig()
if err != nil {
return nil, err
}
}
} else {
te.ControlPlane = integration.ControlPlane{}
te.ControlPlane.APIServer = &integration.APIServer{Args: defaultKubeAPIServerFlags}
te.ControlPlane.Etcd = &integration.Etcd{}
if os.Getenv(envKubeAPIServerBin) == "" {
te.ControlPlane.APIServer.Path = defaultAssetPath("kube-apiserver")
}
if os.Getenv(envEtcdBin) == "" {
te.ControlPlane.Etcd.Path = defaultAssetPath("etcd")
}
if os.Getenv(envKubectlBin) == "" {
// we can't just set the path manually (it's behind a function), so set the environment variable instead
if err := os.Setenv(envKubectlBin, defaultAssetPath("kubectl")); err != nil {
return nil, err
}
}
if err := te.defaultTimeouts(); err != nil {
return nil, fmt.Errorf("failed to default controlplane timeouts: %v", err)
}
te.ControlPlane.Etcd.StartTimeout = te.ControlPlaneStartTimeout
te.ControlPlane.Etcd.StopTimeout = te.ControlPlaneStopTimeout
te.ControlPlane.APIServer.StartTimeout = te.ControlPlaneStartTimeout
te.ControlPlane.APIServer.StopTimeout = te.ControlPlaneStopTimeout
if err := te.startControlPlane(); err != nil {
return nil, err
}
// Create the *rest.Config for creating new clients
te.Config = &rest.Config{
Host: te.ControlPlane.APIURL().Host,
}
}
_, err := InstallCRDs(te.Config, CRDInstallOptions{
Paths: te.CRDDirectoryPaths,
CRDs: te.CRDs,
})
return te.Config, err
}
func (te *Environment) startControlPlane() error {
numTries, maxRetries := 0, 5
for ; numTries < maxRetries; numTries++ {
// Start the control plane - retry if it fails
err := te.ControlPlane.Start()
if err == nil {
break
}
// code snippet copied from following answer on stackoverflow
// https://stackoverflow.com/questions/51151973/catching-bind-address-already-in-use-in-golang
if opErr, ok := err.(*net.OpError); ok {
if opErr.Op == "listen" && strings.Contains(opErr.Error(), "address already in use") {
if stopErr := te.ControlPlane.Stop(); stopErr != nil {
return fmt.Errorf("failed to stop controlplane in response to bind error 'address already in use'")
}
}
} else {
return err
}
}
if numTries == maxRetries {
return fmt.Errorf("failed to start the controlplane. retried %d times", numTries)
}
return nil
}
func (te *Environment) defaultTimeouts() error {
var err error
if te.ControlPlaneStartTimeout == 0 {
if envVal := os.Getenv(envStartTimeout); envVal != "" {
te.ControlPlaneStartTimeout, err = time.ParseDuration(envVal)
if err != nil {
return err
}
} else {
te.ControlPlaneStartTimeout = defaultKubebuilderControlPlaneStartTimeout
}
}
if te.ControlPlaneStopTimeout == 0 {
if envVal := os.Getenv(envStopTimeout); envVal != "" {
te.ControlPlaneStopTimeout, err = time.ParseDuration(envVal)
if err != nil {
return err
}
} else {
te.ControlPlaneStopTimeout = defaultKubebuilderControlPlaneStopTimeout
}
}
return nil
}

25
vendor/sigs.k8s.io/controller-runtime/pkg/event/doc.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
/*
Copyright 2018 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 event contains the definitions for the Event types produced by source.Sources and transformed into
reconcile.Requests by handler.EventHandler.
The details of how events are produced and transformed into reconcile.Requests are not something most
users should need to use or understand. Instead of working with Events, users should use
source.Sources and handler.EventHandlers with Controller.Watch.
*/
package event

View File

@ -0,0 +1,73 @@
/*
Copyright 2018 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 event
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// CreateEvent is an event where a Kubernetes object was created. CreateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type CreateEvent struct {
// Meta is the ObjectMeta of the Kubernetes Type that was created
Meta v1.Object
// Object is the object from the event
Object runtime.Object
}
// UpdateEvent is an event where a Kubernetes object was updated. UpdateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type UpdateEvent struct {
// MetaOld is the ObjectMeta of the Kubernetes Type that was updated (before the update)
MetaOld v1.Object
// ObjectOld is the object from the event
ObjectOld runtime.Object
// MetaNew is the ObjectMeta of the Kubernetes Type that was updated (after the update)
MetaNew v1.Object
// ObjectNew is the object from the event
ObjectNew runtime.Object
}
// DeleteEvent is an event where a Kubernetes object was deleted. DeleteEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type DeleteEvent struct {
// Meta is the ObjectMeta of the Kubernetes Type that was deleted
Meta v1.Object
// Object is the object from the event
Object runtime.Object
// DeleteStateUnknown is true if the Delete event was missed but we identified the object
// as having been deleted.
DeleteStateUnknown bool
}
// GenericEvent is an event where the operation type is unknown (e.g. polling or event originating outside the cluster).
// GenericEvent should be generated by a source.Source and transformed into a reconcile.Request by an
// handler.EventHandler.
type GenericEvent struct {
// Meta is the ObjectMeta of a Kubernetes Type this event is for
Meta v1.Object
// Object is the object from the event
Object runtime.Object
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2018 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 handler defines EventHandlers that enqueue reconcile.Requests in response to Create, Update, Deletion Events
observed from Watching Kubernetes APIs. Users should provide a source.Source and handler.EventHandler to
Controller.Watch in order to generate and enqueue reconcile.Request work items.
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
reconciled.
EnqueueRequestForOwner - Enqueues a reconcile.Request containing the Name and Namespace of the Owner of the object in the Event.
This will cause owner of the object that was the source of the Event (e.g. the owner object that created the object)
to be reconciled.
EnqueueRequestsFromMapFunc - Enqueues Reconciler.Requests resulting from a user provided transformation function run against the
object in the Event. This will cause an arbitrary collection of objects (defined from a transformation of the
source object) to be reconciled.
*/
package handler

View File

@ -0,0 +1,91 @@
/*
Copyright 2018 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 handler
import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
var enqueueLog = logf.KBLog.WithName("eventhandler").WithName("EnqueueRequestForObject")
var _ EventHandler = &EnqueueRequestForObject{}
// EnqueueRequestForObject enqueues a Request containing the Name and Namespace of the object that is the source of the Event.
// (e.g. the created / deleted / updated objects Name and Namespace). handler.EnqueueRequestForObject is used by almost all
// Controllers that have associated Resources (e.g. CRDs) to reconcile the associated Resource.
type EnqueueRequestForObject struct{}
// Create implements EventHandler
func (e *EnqueueRequestForObject) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
if evt.Meta == nil {
enqueueLog.Error(nil, "CreateEvent received with no metadata", "CreateEvent", evt)
return
}
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.Meta.GetName(),
Namespace: evt.Meta.GetNamespace(),
}})
}
// Update implements EventHandler
func (e *EnqueueRequestForObject) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
if evt.MetaOld != nil {
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.MetaOld.GetName(),
Namespace: evt.MetaOld.GetNamespace(),
}})
} else {
enqueueLog.Error(nil, "UpdateEvent received with no old metadata", "UpdateEvent", evt)
}
if evt.MetaNew != nil {
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.MetaNew.GetName(),
Namespace: evt.MetaNew.GetNamespace(),
}})
} else {
enqueueLog.Error(nil, "UpdateEvent received with no new metadata", "UpdateEvent", evt)
}
}
// Delete implements EventHandler
func (e *EnqueueRequestForObject) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
if evt.Meta == nil {
enqueueLog.Error(nil, "DeleteEvent received with no metadata", "DeleteEvent", evt)
return
}
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.Meta.GetName(),
Namespace: evt.Meta.GetNamespace(),
}})
}
// Generic implements EventHandler
func (e *EnqueueRequestForObject) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
if evt.Meta == nil {
enqueueLog.Error(nil, "GenericEvent received with no metadata", "GenericEvent", evt)
return
}
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.Meta.GetName(),
Namespace: evt.Meta.GetNamespace(),
}})
}

View File

@ -0,0 +1,94 @@
/*
Copyright 2018 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 handler
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ EventHandler = &EnqueueRequestsFromMapFunc{}
// EnqueueRequestsFromMapFunc enqueues Requests by running a transformation function that outputs a collection
// of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects
// defined by some user specified transformation of the source Event. (e.g. trigger Reconciler for a set of objects
// in response to a cluster resize event caused by adding or deleting a Node)
//
// EnqueueRequestsFromMapFunc is frequently used to fan-out updates from one object to one or more other
// objects of a differing type.
//
// For UpdateEvents which contain both a new and old object, the transformation function is run on both
// objects and both sets of Requests are enqueue.
type EnqueueRequestsFromMapFunc struct {
// Mapper transforms the argument into a slice of keys to be reconciled
ToRequests Mapper
}
// Create implements EventHandler
func (e *EnqueueRequestsFromMapFunc) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
e.mapAndEnqueue(q, MapObject{Meta: evt.Meta, Object: evt.Object})
}
// Update implements EventHandler
func (e *EnqueueRequestsFromMapFunc) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
e.mapAndEnqueue(q, MapObject{Meta: evt.MetaOld, Object: evt.ObjectOld})
e.mapAndEnqueue(q, MapObject{Meta: evt.MetaNew, Object: evt.ObjectNew})
}
// Delete implements EventHandler
func (e *EnqueueRequestsFromMapFunc) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
e.mapAndEnqueue(q, MapObject{Meta: evt.Meta, Object: evt.Object})
}
// Generic implements EventHandler
func (e *EnqueueRequestsFromMapFunc) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
e.mapAndEnqueue(q, MapObject{Meta: evt.Meta, Object: evt.Object})
}
func (e *EnqueueRequestsFromMapFunc) mapAndEnqueue(q workqueue.RateLimitingInterface, object MapObject) {
for _, req := range e.ToRequests.Map(object) {
q.Add(req)
}
}
// Mapper maps an object to a collection of keys to be enqueued
type Mapper interface {
// Map maps an object
Map(MapObject) []reconcile.Request
}
// MapObject contains information from an event to be transformed into a Request.
type MapObject struct {
// Meta is the meta data for an object from an event.
Meta metav1.Object
// Object is the object from an event.
Object runtime.Object
}
var _ Mapper = ToRequestsFunc(nil)
// ToRequestsFunc implements Mapper using a function.
type ToRequestsFunc func(MapObject) []reconcile.Request
// Map implements Mapper
func (m ToRequestsFunc) Map(i MapObject) []reconcile.Request {
return m(i)
}

View File

@ -0,0 +1,165 @@
/*
Copyright 2018 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 handler
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
var _ EventHandler = &EnqueueRequestForOwner{}
var log = logf.KBLog.WithName("eventhandler").WithName("EnqueueRequestForOwner")
// EnqueueRequestForOwner enqueues Requests for the Owners of an object. E.g. the object that created
// the object that was the source of the Event.
//
// If a ReplicaSet creates Pods, users may reconcile the ReplicaSet in response to Pod Events using:
//
// - a source.Kind Source with Type of Pod.
//
// - a handler.EnqueueRequestForOwner EventHandler with an OwnerType of ReplicaSet and IsController set to true.
type EnqueueRequestForOwner struct {
// OwnerType is the type of the Owner object to look for in OwnerReferences. Only Group and Kind are compared.
OwnerType runtime.Object
// IsController if set will only look at the first OwnerReference with Controller: true.
IsController bool
// groupKind is the cached Group and Kind from OwnerType
groupKind schema.GroupKind
}
// Create implements EventHandler
func (e *EnqueueRequestForOwner) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
for _, req := range e.getOwnerReconcileRequest(evt.Meta) {
q.Add(req)
}
}
// Update implements EventHandler
func (e *EnqueueRequestForOwner) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
for _, req := range e.getOwnerReconcileRequest(evt.MetaOld) {
q.Add(req)
}
for _, req := range e.getOwnerReconcileRequest(evt.MetaNew) {
q.Add(req)
}
}
// Delete implements EventHandler
func (e *EnqueueRequestForOwner) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
for _, req := range e.getOwnerReconcileRequest(evt.Meta) {
q.Add(req)
}
}
// Generic implements EventHandler
func (e *EnqueueRequestForOwner) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
for _, req := range e.getOwnerReconcileRequest(evt.Meta) {
q.Add(req)
}
}
// parseOwnerTypeGroupKind parses the OwnerType into a Group and Kind and caches the result. Returns false
// if the OwnerType could not be parsed using the scheme.
func (e *EnqueueRequestForOwner) parseOwnerTypeGroupKind(scheme *runtime.Scheme) error {
// Get the kinds of the type
kinds, _, err := scheme.ObjectKinds(e.OwnerType)
if err != nil {
log.Error(err, "Could not get ObjectKinds for OwnerType", "OwnerType", e.OwnerType)
return err
}
// Expect only 1 kind. If there is more than one kind this is probably an edge case such as ListOptions.
if len(kinds) != 1 {
err := fmt.Errorf("Expected exactly 1 kind for OwnerType %T, but found %s kinds", e.OwnerType, kinds)
log.Error(err, "", "OwnerType", e.OwnerType, "Kinds", kinds)
return err
}
// Cache the Group and Kind for the OwnerType
e.groupKind = schema.GroupKind{Group: kinds[0].Group, Kind: kinds[0].Kind}
return nil
}
// getOwnerReconcileRequest looks at object and returns a slice of reconcile.Request to reconcile
// owners of object that match e.OwnerType.
func (e *EnqueueRequestForOwner) getOwnerReconcileRequest(object metav1.Object) []reconcile.Request {
// Iterate through the OwnerReferences looking for a match on Group and Kind against what was requested
// by the user
var result []reconcile.Request
for _, ref := range e.getOwnersReferences(object) {
// Parse the Group out of the OwnerReference to compare it to what was parsed out of the requested OwnerType
refGV, err := schema.ParseGroupVersion(ref.APIVersion)
if err != nil {
log.Error(err, "Could not parse OwnerReference GroupVersion",
"OwnerReference", ref.APIVersion)
return nil
}
// Compare the OwnerReference Group and Kind against the OwnerType Group and Kind specified by the user.
// If the two match, create a Request for the objected referred to by
// the OwnerReference. Use the Name from the OwnerReference and the Namespace from the
// object in the event.
if ref.Kind == e.groupKind.Kind && refGV.Group == e.groupKind.Group {
// Match found - add a Request for the object referred to in the OwnerReference
result = append(result, reconcile.Request{NamespacedName: types.NamespacedName{
Namespace: object.GetNamespace(),
Name: ref.Name,
}})
}
}
// Return the matches
return result
}
// getOwnersReferences returns the OwnerReferences for an object as specified by the EnqueueRequestForOwner
// - if IsController is true: only take the Controller OwnerReference (if found)
// - if IsController is false: take all OwnerReferences
func (e *EnqueueRequestForOwner) getOwnersReferences(object metav1.Object) []metav1.OwnerReference {
if object == nil {
return nil
}
// If not filtered as Controller only, then use all the OwnerReferences
if !e.IsController {
return object.GetOwnerReferences()
}
// If filtered to a Controller, only take the Controller OwnerReference
if ownerRef := metav1.GetControllerOf(object); ownerRef != nil {
return []metav1.OwnerReference{*ownerRef}
}
// No Controller OwnerReference found
return nil
}
var _ inject.Scheme = &EnqueueRequestForOwner{}
// InjectScheme is called by the Controller to provide a singleton scheme to the EnqueueRequestForOwner.
func (e *EnqueueRequestForOwner) InjectScheme(s *runtime.Scheme) error {
return e.parseOwnerTypeGroupKind(s)
}

View File

@ -0,0 +1,104 @@
/*
Copyright 2018 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 handler
import (
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/event"
)
// EventHandler enqueues reconcile.Requests in response to events (e.g. Pod Create). EventHandlers map an Event
// for one object to trigger Reconciles for either the same object or different objects - e.g. if there is an
// Event for object with type Foo (using source.KindSource) then reconcile one or more object(s) with type Bar.
//
// Identical reconcile.Requests will be batched together through the queuing mechanism before reconcile is called.
//
// * Use EnqueueRequestForObject to reconcile the object the event is for
// - do this for events for the type the Controller Reconciles. (e.g. Deployment for a Deployment Controller)
//
// * Use EnqueueRequestForOwner to reconcile the owner of the object the event is for
// - do this for events for the types the Controller creates. (e.g. ReplicaSets created by a Deployment Controller)
//
// * Use EnqueueRequestFromMapFunc to transform an event for an object to a reconcile of an object
// of a different type - do this for events for types the Controller may be interested in, but doesn't create.
// (e.g. If Foo responds to cluster size events, map Node events to Foo objects.)
//
// Unless you are implementing your own EventHandler, you can ignore the functions on the EventHandler interface.
// Most users shouldn't need to implement their own EventHandler.
type EventHandler interface {
// Create is called in response to an create event - e.g. Pod Creation.
Create(event.CreateEvent, workqueue.RateLimitingInterface)
// Update is called in response to an update event - e.g. Pod Updated.
Update(event.UpdateEvent, workqueue.RateLimitingInterface)
// Delete is called in response to a delete event - e.g. Pod Deleted.
Delete(event.DeleteEvent, workqueue.RateLimitingInterface)
// Generic is called in response to an event of an unknown type or a synthetic event triggered as a cron or
// external trigger request - e.g. reconcile Autoscaling, or a Webhook.
Generic(event.GenericEvent, workqueue.RateLimitingInterface)
}
var _ EventHandler = Funcs{}
// Funcs implements EventHandler.
type Funcs struct {
// Create is called in response to an add event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
CreateFunc func(event.CreateEvent, workqueue.RateLimitingInterface)
// Update is called in response to an update event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
UpdateFunc func(event.UpdateEvent, workqueue.RateLimitingInterface)
// Delete is called in response to a delete event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
DeleteFunc func(event.DeleteEvent, workqueue.RateLimitingInterface)
// GenericFunc is called in response to a generic event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
GenericFunc func(event.GenericEvent, workqueue.RateLimitingInterface)
}
// Create implements EventHandler
func (h Funcs) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) {
if h.CreateFunc != nil {
h.CreateFunc(e, q)
}
}
// Delete implements EventHandler
func (h Funcs) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) {
if h.DeleteFunc != nil {
h.DeleteFunc(e, q)
}
}
// Update implements EventHandler
func (h Funcs) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) {
if h.UpdateFunc != nil {
h.UpdateFunc(e, q)
}
}
// Generic implements EventHandler
func (h Funcs) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) {
if h.GenericFunc != nil {
h.GenericFunc(e, q)
}
}

View File

@ -0,0 +1,35 @@
/*
Copyright 2018 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 handler_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestEventhandler(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "Eventhandler Suite", []Reporter{envtest.NewlineReporter{}})
}
var _ = BeforeSuite(func() {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
})

View File

@ -0,0 +1,903 @@
/*
Copyright 2018 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 handler_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/controller/controllertest"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ = Describe("Eventhandler", func() {
var q workqueue.RateLimitingInterface
var instance handler.EnqueueRequestForObject
var pod *corev1.Pod
t := true
BeforeEach(func() {
q = controllertest.Queue{Interface: workqueue.New()}
instance = handler.EnqueueRequestForObject{}
pod = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "baz"},
}
})
Describe("EnqueueRequestForObject", func() {
It("should enqueue a Request with the Name / Namespace of the object in the CreateEvent.", func(done Done) {
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).NotTo(BeNil())
req, ok := i.(reconcile.Request)
Expect(ok).To(BeTrue())
Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz", Name: "baz"}))
close(done)
})
It("should enqueue a Request with the Name / Namespace of the object in the DeleteEvent.", func(done Done) {
evt := event.DeleteEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Delete(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).NotTo(BeNil())
req, ok := i.(reconcile.Request)
Expect(ok).To(BeTrue())
Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz", Name: "baz"}))
close(done)
})
It("should enqueue a Request with the Name / Namespace of both objects in the UpdateEvent.",
func(done Done) {
newPod := pod.DeepCopy()
newPod.Name = "baz2"
newPod.Namespace = "biz2"
evt := event.UpdateEvent{
ObjectOld: pod,
MetaOld: pod.GetObjectMeta(),
ObjectNew: newPod,
MetaNew: newPod.GetObjectMeta(),
}
instance.Update(evt, q)
Expect(q.Len()).To(Equal(2))
i, _ := q.Get()
Expect(i).NotTo(BeNil())
req, ok := i.(reconcile.Request)
Expect(ok).To(BeTrue())
Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz", Name: "baz"}))
i, _ = q.Get()
Expect(i).NotTo(BeNil())
req, ok = i.(reconcile.Request)
Expect(ok).To(BeTrue())
Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz2", Name: "baz2"}))
close(done)
})
It("should enqueue a Request with the Name / Namespace of the object in the GenericEvent.", func(done Done) {
evt := event.GenericEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Generic(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).NotTo(BeNil())
req, ok := i.(reconcile.Request)
Expect(ok).To(BeTrue())
Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz", Name: "baz"}))
close(done)
})
Context("for a runtime.Object without Metadata", func() {
It("should do nothing if the Metadata is missing for a CreateEvent.", func(done Done) {
evt := event.CreateEvent{
Object: pod,
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
close(done)
})
It("should do nothing if the Metadata is missing for a UpdateEvent.", func(done Done) {
newPod := pod.DeepCopy()
newPod.Name = "baz2"
newPod.Namespace = "biz2"
evt := event.UpdateEvent{
ObjectNew: newPod,
MetaNew: newPod.GetObjectMeta(),
ObjectOld: pod,
}
instance.Update(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).NotTo(BeNil())
req, ok := i.(reconcile.Request)
Expect(ok).To(BeTrue())
Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz2", Name: "baz2"}))
evt.MetaNew = nil
evt.MetaOld = pod.GetObjectMeta()
instance.Update(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ = q.Get()
Expect(i).NotTo(BeNil())
req, ok = i.(reconcile.Request)
Expect(ok).To(BeTrue())
Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz", Name: "baz"}))
close(done)
})
It("should do nothing if the Metadata is missing for a DeleteEvent.", func(done Done) {
evt := event.DeleteEvent{
Object: pod,
}
instance.Delete(evt, q)
Expect(q.Len()).To(Equal(0))
close(done)
})
It("should do nothing if the Metadata is missing for a GenericEvent.", func(done Done) {
evt := event.GenericEvent{
Object: pod,
}
instance.Generic(evt, q)
Expect(q.Len()).To(Equal(0))
close(done)
})
})
})
Describe("EnqueueRequestsFromMapFunc", func() {
It("should enqueue a Request with the function applied to the CreateEvent.", func() {
req := []reconcile.Request{}
instance := handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
defer GinkgoRecover()
Expect(a.Meta).To(Equal(pod.GetObjectMeta()))
Expect(a.Object).To(Equal(pod))
req = []reconcile.Request{
{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"},
},
{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"},
},
}
return req
}),
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(2))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"}}))
})
It("should enqueue a Request with the function applied to the DeleteEvent.", func() {
req := []reconcile.Request{}
instance := handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
defer GinkgoRecover()
Expect(a.Meta).To(Equal(pod.GetObjectMeta()))
Expect(a.Object).To(Equal(pod))
req = []reconcile.Request{
{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"},
},
{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"},
},
}
return req
}),
}
evt := event.DeleteEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Delete(evt, q)
Expect(q.Len()).To(Equal(2))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"}}))
})
It("should enqueue a Request with the function applied to both objects in the UpdateEvent.",
func() {
newPod := pod.DeepCopy()
newPod.Name = pod.Name + "2"
newPod.Namespace = pod.Namespace + "2"
req := []reconcile.Request{}
instance := handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
defer GinkgoRecover()
req = []reconcile.Request{
{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: a.Meta.GetName() + "-bar"},
},
{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: a.Meta.GetName() + "-baz"},
},
}
return req
}),
}
evt := event.UpdateEvent{
ObjectOld: pod,
MetaOld: pod.GetObjectMeta(),
ObjectNew: newPod,
MetaNew: newPod.GetObjectMeta(),
}
instance.Update(evt, q)
Expect(q.Len()).To(Equal(4))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "baz-bar"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz-baz"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "baz2-bar"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz2-baz"}}))
})
It("should enqueue a Request with the function applied to the GenericEvent.", func() {
req := []reconcile.Request{}
instance := handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
defer GinkgoRecover()
Expect(a.Meta).To(Equal(pod.GetObjectMeta()))
Expect(a.Object).To(Equal(pod))
req = []reconcile.Request{
{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"},
},
{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"},
},
}
return req
}),
}
evt := event.GenericEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Generic(evt, q)
Expect(q.Len()).To(Equal(2))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"}}))
})
})
Describe("EnqueueRequestForOwner", func() {
It("should enqueue a Request with the Owner of the object in the CreateEvent.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}}))
})
It("should enqueue a Request with the Owner of the object in the DeleteEvent.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.DeleteEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Delete(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}}))
})
It("should enqueue a Request with the Owners of both objects in the UpdateEvent.", func() {
newPod := pod.DeepCopy()
newPod.Name = pod.Name + "2"
newPod.Namespace = pod.Namespace + "2"
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
newPod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo2-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.UpdateEvent{
ObjectOld: pod,
MetaOld: pod.GetObjectMeta(),
ObjectNew: newPod,
MetaNew: newPod.GetObjectMeta(),
}
instance.Update(evt, q)
Expect(q.Len()).To(Equal(2))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo1-parent"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: newPod.GetNamespace(), Name: "foo2-parent"}}))
})
It("should enqueue a Request with the Owner of the object in the GenericEvent.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.GenericEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Generic(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}}))
})
It("should not enqueue a Request if there are no owners matching Group and Kind.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
IsController: t,
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{ // Wrong group
Name: "foo1-parent",
Kind: "ReplicaSet",
APIVersion: "extensions/v1",
},
{ // Wrong kind
Name: "foo2-parent",
Kind: "Deployment",
APIVersion: "apps/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
It("should enqueue a Request if there are owners matching Group "+
"and Kind with a different version.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v2",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}}))
})
It("should not enqueue a Request if there are no owners.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
Context("with the Controller field set to true", func() {
It("should enqueue reconcile.Requests for only the first the Controller if there are "+
"multiple Controller owners.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
IsController: t,
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
{
Name: "foo2-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
Controller: &t,
},
{
Name: "foo3-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
{
Name: "foo4-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
Controller: &t,
},
{
Name: "foo5-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(1))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo2-parent"}}))
})
It("should not enqueue reconcile.Requests if there are no Controller owners.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
IsController: t,
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
{
Name: "foo2-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
{
Name: "foo3-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
It("should not enqueue reconcile.Requests if there are no owners.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
IsController: t,
}
instance.InjectScheme(scheme.Scheme)
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
})
Context("with the Controller field set to false", func() {
It("should enqueue a reconcile.Requests for all owners.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
{
Name: "foo2-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
{
Name: "foo3-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(3))
i, _ := q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo1-parent"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo2-parent"}}))
i, _ = q.Get()
Expect(i).To(Equal(reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo3-parent"}}))
})
})
Context("with a nil metadata object", func() {
It("should do nothing.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1",
},
}
evt := event.CreateEvent{
Object: pod,
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
})
Context("with a multiple matching kinds", func() {
It("should do nothing.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &metav1.ListOptions{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ListOptions",
APIVersion: "meta/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
})
Context("with an OwnerType that cannot be resolved", func() {
It("should do nothing.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &controllertest.ErrorType{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ListOptions",
APIVersion: "meta/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
})
Context("with a nil OwnerType", func() {
It("should do nothing.", func() {
instance := handler.EnqueueRequestForOwner{}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "OwnerType",
APIVersion: "meta/v1",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
})
Context("with an invalid APIVersion in the OwnerReference", func() {
It("should do nothing.", func() {
instance := handler.EnqueueRequestForOwner{
OwnerType: &appsv1.ReplicaSet{},
}
instance.InjectScheme(scheme.Scheme)
pod.OwnerReferences = []metav1.OwnerReference{
{
Name: "foo1-parent",
Kind: "ReplicaSet",
APIVersion: "apps/v1/fail",
},
}
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
Expect(q.Len()).To(Equal(0))
})
})
})
Describe("Funcs", func() {
failingFuncs := handler.Funcs{
CreateFunc: func(event.CreateEvent, workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Fail("Did not expect CreateEvent to be called.")
},
DeleteFunc: func(event.DeleteEvent, workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Fail("Did not expect DeleteEvent to be called.")
},
UpdateFunc: func(event.UpdateEvent, workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Fail("Did not expect UpdateEvent to be called.")
},
GenericFunc: func(event.GenericEvent, workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Fail("Did not expect GenericEvent to be called.")
},
}
It("should call CreateFunc for a CreateEvent if provided.", func(done Done) {
instance := failingFuncs
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.CreateFunc = func(evt2 event.CreateEvent, q2 workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Expect(q2).To(Equal(q))
Expect(evt2).To(Equal(evt))
}
instance.Create(evt, q)
close(done)
})
It("should NOT call CreateFunc for a CreateEvent if NOT provided.", func(done Done) {
instance := failingFuncs
instance.CreateFunc = nil
evt := event.CreateEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Create(evt, q)
close(done)
})
It("should call UpdateFunc for an UpdateEvent if provided.", func(done Done) {
newPod := pod.DeepCopy()
newPod.Name = pod.Name + "2"
newPod.Namespace = pod.Namespace + "2"
evt := event.UpdateEvent{
ObjectOld: pod,
MetaOld: pod.GetObjectMeta(),
ObjectNew: newPod,
MetaNew: newPod.GetObjectMeta(),
}
instance := failingFuncs
instance.UpdateFunc = func(evt2 event.UpdateEvent, q2 workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Expect(q2).To(Equal(q))
Expect(evt2).To(Equal(evt))
}
instance.Update(evt, q)
close(done)
})
It("should NOT call UpdateFunc for an UpdateEvent if NOT provided.", func(done Done) {
newPod := pod.DeepCopy()
newPod.Name = pod.Name + "2"
newPod.Namespace = pod.Namespace + "2"
evt := event.UpdateEvent{
ObjectOld: pod,
MetaOld: pod.GetObjectMeta(),
ObjectNew: newPod,
MetaNew: newPod.GetObjectMeta(),
}
instance.Update(evt, q)
close(done)
})
It("should call DeleteFunc for a DeleteEvent if provided.", func(done Done) {
instance := failingFuncs
evt := event.DeleteEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.DeleteFunc = func(evt2 event.DeleteEvent, q2 workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Expect(q2).To(Equal(q))
Expect(evt2).To(Equal(evt))
}
instance.Delete(evt, q)
close(done)
})
It("should NOT call DeleteFunc for a DeleteEvent if NOT provided.", func(done Done) {
instance := failingFuncs
instance.DeleteFunc = nil
evt := event.DeleteEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Delete(evt, q)
close(done)
})
It("should call GenericFunc for a GenericEvent if provided.", func(done Done) {
instance := failingFuncs
evt := event.GenericEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.GenericFunc = func(evt2 event.GenericEvent, q2 workqueue.RateLimitingInterface) {
defer GinkgoRecover()
Expect(q2).To(Equal(q))
Expect(evt2).To(Equal(evt))
}
instance.Generic(evt, q)
close(done)
})
It("should NOT call GenericFunc for a GenericEvent if NOT provided.", func(done Done) {
instance := failingFuncs
instance.GenericFunc = nil
evt := event.GenericEvent{
Object: pod,
Meta: pod.GetObjectMeta(),
}
instance.Generic(evt, q)
close(done)
})
})
})

View File

@ -0,0 +1,110 @@
/*
Copyright 2018 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 handler_test
import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var c controller.Controller
// This example watches Pods and enqueues Requests with the Name and Namespace of the Pod from
// the Event (i.e. change caused by a Create, Update, Delete).
func ExampleEnqueueRequestForObject() {
// controller is a controller.controller
c.Watch(
&source.Kind{Type: &corev1.Pod{}},
&handler.EnqueueRequestForObject{},
)
}
// This example watches ReplicaSets and enqueues a Request containing the Name and Namespace of the
// owning (direct) Deployment responsible for the creation of the ReplicaSet.
func ExampleEnqueueRequestForOwner() {
// controller is a controller.controller
c.Watch(
&source.Kind{Type: &appsv1.ReplicaSet{}},
&handler.EnqueueRequestForOwner{
OwnerType: &appsv1.Deployment{},
IsController: true,
},
)
}
// This example watches Deployments and enqueues a Request contain the Name and Namespace of different
// objects (of Type: MyKind) using a mapping function defined by the user.
func ExampleEnqueueRequestsFromMapFunc() {
// controller is a controller.controller
c.Watch(
&source.Kind{Type: &appsv1.Deployment{}},
&handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
return []reconcile.Request{
{NamespacedName: types.NamespacedName{
Name: a.Meta.GetName() + "-1",
Namespace: a.Meta.GetNamespace(),
}},
{NamespacedName: types.NamespacedName{
Name: a.Meta.GetName() + "-2",
Namespace: a.Meta.GetNamespace(),
}},
}
}),
})
}
// This example implements handler.EnqueueRequestForObject.
func ExampleFuncs() {
// controller is a controller.controller
c.Watch(
&source.Kind{Type: &corev1.Pod{}},
handler.Funcs{
CreateFunc: func(e event.CreateEvent, q workqueue.RateLimitingInterface) {
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: e.Meta.GetName(),
Namespace: e.Meta.GetNamespace(),
}})
},
UpdateFunc: func(e event.UpdateEvent, q workqueue.RateLimitingInterface) {
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: e.MetaNew.GetName(),
Namespace: e.MetaNew.GetNamespace(),
}})
},
DeleteFunc: func(e event.DeleteEvent, q workqueue.RateLimitingInterface) {
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: e.Meta.GetName(),
Namespace: e.Meta.GetNamespace(),
}})
},
GenericFunc: func(e event.GenericEvent, q workqueue.RateLimitingInterface) {
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: e.Meta.GetName(),
Namespace: e.Meta.GetNamespace(),
}})
},
},
)
}

View File

@ -0,0 +1,48 @@
/*
Copyright 2017 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 admission
import (
"fmt"
"k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
// Decode reads the Raw data from review and deserializes it into object returning a non-nil response if there was an
// error
func Decode(review v1beta1.AdmissionReview, object runtime.Object,
resourceType metav1.GroupVersionResource) *v1beta1.AdmissionResponse {
if review.Request.Resource != resourceType {
return ErrorResponse(fmt.Errorf("expect resource to be %s", resourceType))
}
raw := review.Request.Object.Raw
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(raw, nil, object); err != nil {
fmt.Printf("%v", err)
return ErrorResponse(err)
}
return nil
}

View File

@ -0,0 +1,18 @@
/*
Copyright 2017 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 admission provides libraries for creating admission webhooks.
package admission

View File

@ -0,0 +1,42 @@
/*
Copyright 2017 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 admission_test
import (
"fmt"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/internal/admission"
)
func ExampleFunc() {
var _ admission.Func = func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
pod := corev1.Pod{}
resourceType := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
if errResp := admission.Decode(review, &pod, resourceType); errResp != nil {
return errResp
}
// Business logic for admission decision
if len(pod.Spec.Containers) != 1 {
return admission.DenyResponse(fmt.Sprintf(
"pod %s/%s may only have 1 container.", pod.Namespace, pod.Name))
}
return admission.AllowResponse()
}
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2017 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 admission_test
import (
"fmt"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/internal/admission"
)
func ExampleDecode() {
var review v1beta1.AdmissionReview
resourceType := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
pod := corev1.Pod{}
if errResp := admission.Decode(review, &pod, resourceType); errResp != nil {
// Send error resp
}
}
func ExampleErrorResponse() {
admission.ErrorResponse(fmt.Errorf("some error explanation"))
}
func ExampleDenyResponse() {
admission.DenyResponse(fmt.Sprintf("some deny explanation"))
}
func ExampleAllowResponse() {
admission.AllowResponse()
}

View File

@ -0,0 +1,59 @@
/*
Copyright 2017 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 admission_test
import (
"fmt"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/internal/admission"
)
func ExampleHandleFunc() {
resourceType := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
admission.HandleFunc("/pod", resourceType, func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
pod := corev1.Pod{}
if errResp := admission.Decode(review, &pod, resourceType); errResp != nil {
return errResp
}
// Business logic for admission decision
if len(pod.Spec.Containers) != 1 {
return admission.DenyResponse(fmt.Sprintf(
"pod %s/%s may only have 1 container.", pod.Namespace, pod.Name))
}
return admission.AllowResponse()
})
}
func ExampleManager_HandleFunc() {
resourceType := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
ah := admission.Manager{}
ah.HandleFunc("/pod", resourceType, func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
pod := corev1.Pod{}
if errResp := admission.Decode(review, &pod, resourceType); errResp != nil {
return errResp
}
// Business logic for admission decision
if len(pod.Spec.Containers) != 1 {
return admission.DenyResponse(fmt.Sprintf(
"pod %s/%s may only have 1 container.", pod.Namespace, pod.Name))
}
return admission.AllowResponse()
})
}

View File

@ -0,0 +1,43 @@
/*
Copyright 2017 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 admission_test
import (
"fmt"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/internal/admission"
)
func Example() {
resourceType := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
admission.HandleFunc("/pod", resourceType, func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
pod := corev1.Pod{}
if errResp := admission.Decode(review, &pod, resourceType); errResp != nil {
return errResp
}
// Business logic for admission decision
if len(pod.Spec.Containers) != 1 {
return admission.DenyResponse(fmt.Sprintf(
"pod %s/%s may only have 1 container.", pod.Namespace, pod.Name))
}
return admission.AllowResponse()
})
admission.ListenAndServeTLS("")
}

View File

@ -0,0 +1,72 @@
/*
Copyright 2017 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 admission
import (
"net/http"
"k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Func implements an AdmissionReview operation for a GroupVersionResource
type Func func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse
// HandleEntry
type admissionHandler struct {
GVR metav1.GroupVersionResource
Fn Func
}
// handle handles an admission request and returns a result
func (ah admissionHandler) handle(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
return ah.handle(review)
}
// Manager manages admission controllers
type Manager struct {
Entries map[string]admissionHandler
SMux *http.ServeMux
}
// DefaultAdmissionFns is the default admission control functions registry
var DefaultAdmissionFns = &Manager{
SMux: http.DefaultServeMux,
}
// HandleFunc registers fn as an admission control webhook callback for the group,version,resources specified
func (e *Manager) HandleFunc(path string, gvr metav1.GroupVersionResource, fn Func) {
// Register the entry so a Webhook config is created
e.Entries[path] = admissionHandler{gvr, fn}
// Register the handler path
e.SMux.Handle(path, httpHandler{fn})
}
// HandleFunc registers fn as an admission control webhook callback for the group,version,resources specified
func HandleFunc(path string, gvr metav1.GroupVersionResource, fn Func) {
DefaultAdmissionFns.HandleFunc(path, gvr, fn)
}
// ListenAndServeTLS starts the admission HttpServer.
func ListenAndServeTLS(addr string) error {
server := &http.Server{
Addr: addr,
TLSConfig: nil, // TODO: Set this
}
return server.ListenAndServeTLS("", "")
}

View File

@ -0,0 +1,80 @@
/*
Copyright 2017 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 admission
import (
"encoding/json"
"io/ioutil"
"net/http"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
var (
// TODO(directxman12): this shouldn't be a global log
log = logf.KBLog.WithName("admission").WithName("http-handler")
)
func (h httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
log.Error(nil, "invalid content type, expected application/json", "context type", contentType)
return
}
var reviewResponse *v1beta1.AdmissionResponse
ar := v1beta1.AdmissionReview{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
log.Error(err, "unable to decode request body")
reviewResponse = ErrorResponse(err)
} else {
reviewResponse = h.admit(ar)
}
response := v1beta1.AdmissionReview{}
if reviewResponse != nil {
response.Response = reviewResponse
response.Response.UID = ar.Request.UID
}
// reset the Object and OldObject, they are not needed in a response.
ar.Request.Object = runtime.RawExtension{}
ar.Request.OldObject = runtime.RawExtension{}
resp, err := json.Marshal(response)
if err != nil {
log.Error(err, "unable to marshal response")
return
}
if _, err := w.Write(resp); err != nil {
log.Error(err, "unable to write response")
}
}
type httpHandler struct {
admit Func
}

View File

@ -0,0 +1,48 @@
/*
Copyright 2017 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 admission
import (
"k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ErrorResponse creates a new AdmissionResponse for an error handling the request
func ErrorResponse(err error) *v1beta1.AdmissionResponse {
return &v1beta1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
},
}
}
// DenyResponse returns a new response for denying a request
func DenyResponse(msg string) *v1beta1.AdmissionResponse {
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Reason: metav1.StatusReason(msg),
},
}
}
// AllowResponse returns a new response for admitting a request
func AllowResponse() *v1beta1.AdmissionResponse {
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}

View File

@ -0,0 +1,42 @@
/*
Copyright 2017 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 admission
//type certs struct {
// Cert []byte
// Key []byte
// CACert []byte
//}
//// MakeTLSConfig makes a TLS configuration suitable for use with the server
//func makeTLSConfig(certs certs) (*tls.Config, error) {
// caCertPool := x509.NewCertPool()
// caCertPool.AppendCertsFromPEM(certs.CACert)
// //cert, err := tls.X509KeyPair(certs.Cert, certs.Key)
// //if err != nil {
// // return nil, err
// //}
// return &tls.Config{
// //Certificates: []tls.Certificate{cert},
// ClientCAs: caCertPool,
// ClientAuth: tls.NoClientCert,
// // Note on GKE there apparently is no client cert sent, so this
// // does not work on GKE.
// // TODO: make this into a configuration option.
// // ClientAuth: tls.RequireAndVerifyClientCert,
// }, nil
//}

View File

@ -0,0 +1,235 @@
/*
Copyright 2018 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 controller
import (
"fmt"
"sync"
"time"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var log = logf.KBLog.WithName("controller")
var _ inject.Injector = &Controller{}
// Controller implements controller.Controller
type Controller struct {
// Name is used to uniquely identify a Controller in tracing, logging and monitoring. Name is required.
Name string
// MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1.
MaxConcurrentReconciles int
// Reconciler is a function that can be called at any time with the Name / Namespace of an object and
// ensures that the state of the system matches the state specified in the object.
// Defaults to the DefaultReconcileFunc.
Do reconcile.Reconciler
// Client is a lazily initialized Client. The controllerManager will initialize this when Start is called.
Client client.Client
// Scheme is injected by the controllerManager when controllerManager.Start is called
Scheme *runtime.Scheme
// informers are injected by the controllerManager when controllerManager.Start is called
Cache cache.Cache
// Config is the rest.Config used to talk to the apiserver. Defaults to one of in-cluster, environment variable
// specified, or the ~/.kube/Config.
Config *rest.Config
// Queue is an listeningQueue that listens for events from Informers and adds object keys to
// the Queue for processing
Queue workqueue.RateLimitingInterface
// SetFields is used to inject dependencies into other objects such as Sources, EventHandlers and Predicates
SetFields func(i interface{}) error
// mu is used to synchronize Controller setup
mu sync.Mutex
// JitterPeriod allows tests to reduce the JitterPeriod so they complete faster
JitterPeriod time.Duration
// WaitForCacheSync allows tests to mock out the WaitForCacheSync function to return an error
// defaults to Cache.WaitForCacheSync
WaitForCacheSync func(stopCh <-chan struct{}) bool
// Started is true if the Controller has been Started
Started bool
// Recorder is an event recorder for recording Event resources to the
// Kubernetes API.
Recorder record.EventRecorder
// TODO(community): Consider initializing a logger with the Controller Name as the tag
}
// Reconcile implements reconcile.Reconciler
func (c *Controller) Reconcile(r reconcile.Request) (reconcile.Result, error) {
return c.Do.Reconcile(r)
}
// Watch implements controller.Controller
func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prct ...predicate.Predicate) error {
c.mu.Lock()
defer c.mu.Unlock()
// Inject Cache into arguments
if err := c.SetFields(src); err != nil {
return err
}
if err := c.SetFields(evthdler); err != nil {
return err
}
for _, pr := range prct {
if err := c.SetFields(pr); err != nil {
return err
}
}
log.Info("Starting EventSource", "Controller", c.Name, "Source", src)
return src.Start(evthdler, c.Queue, prct...)
}
// Start implements controller.Controller
func (c *Controller) Start(stop <-chan struct{}) error {
c.mu.Lock()
defer c.mu.Unlock()
// TODO(pwittrock): Reconsider HandleCrash
defer utilruntime.HandleCrash()
defer c.Queue.ShutDown()
// Start the SharedIndexInformer factories to begin populating the SharedIndexInformer caches
log.Info("Starting Controller", "Controller", c.Name)
// Wait for the caches to be synced before starting workers
if c.WaitForCacheSync == nil {
c.WaitForCacheSync = c.Cache.WaitForCacheSync
}
if ok := c.WaitForCacheSync(stop); !ok {
// This code is unreachable right now since WaitForCacheSync will never return an error
// Leaving it here because that could happen in the future
err := fmt.Errorf("failed to wait for %s caches to sync", c.Name)
log.Error(err, "Could not wait for Cache to sync", "Controller", c.Name)
return err
}
if c.JitterPeriod == 0 {
c.JitterPeriod = 1 * time.Second
}
// Launch workers to process resources
log.Info("Starting workers", "Controller", c.Name, "WorkerCount", c.MaxConcurrentReconciles)
for i := 0; i < c.MaxConcurrentReconciles; i++ {
// Process work items
go wait.Until(func() {
for c.processNextWorkItem() {
}
}, c.JitterPeriod, stop)
}
c.Started = true
<-stop
log.Info("Stopping workers", "Controller", c.Name)
return nil
}
// processNextWorkItem will read a single work item off the workqueue and
// attempt to process it, by calling the syncHandler.
func (c *Controller) processNextWorkItem() bool {
// This code copy-pasted from the sample-Controller.
obj, shutdown := c.Queue.Get()
if obj == nil {
// Sometimes the Queue gives us nil items when it starts up
c.Queue.Forget(obj)
}
if shutdown {
// Stop working
return false
}
// We call Done here so the workqueue knows we have finished
// processing this item. We also must remember to call Forget if we
// do not want this work item being re-queued. For example, we do
// not call Forget if a transient error occurs, instead the item is
// put back on the workqueue and attempted again after a back-off
// period.
defer c.Queue.Done(obj)
var req reconcile.Request
var ok bool
if req, ok = obj.(reconcile.Request); !ok {
// As the item in the workqueue is actually invalid, we call
// Forget here else we'd go into a loop of attempting to
// process a work item that is invalid.
c.Queue.Forget(obj)
log.Error(nil, "Queue item was not a Request",
"Controller", c.Name, "Type", fmt.Sprintf("%T", obj), "Value", obj)
// Return true, don't take a break
return true
}
// RunInformersAndControllers the syncHandler, passing it the namespace/Name string of the
// resource to be synced.
if result, err := c.Do.Reconcile(req); err != nil {
c.Queue.AddRateLimited(req)
log.Error(err, "Reconciler error", "Controller", c.Name, "Request", req)
return false
} else if result.RequeueAfter > 0 {
c.Queue.AddAfter(req, result.RequeueAfter)
return true
} else if result.Requeue {
c.Queue.AddRateLimited(req)
return true
}
// Finally, if no error occurs we Forget this item so it does not
// get queued again until another change happens.
c.Queue.Forget(obj)
// TODO(directxman12): What does 1 mean? Do we want level constants? Do we want levels at all?
log.V(1).Info("Successfully Reconciled", "Controller", c.Name, "Request", req)
// Return true, don't take a break
return true
}
// InjectFunc implement SetFields.Injector
func (c *Controller) InjectFunc(f inject.Func) error {
c.SetFields = f
return nil
}

View File

@ -0,0 +1,56 @@
/*
Copyright 2018 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 controller
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)
func TestSource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t, "Controller Integration Suite", []Reporter{envtest.NewlineReporter{}})
}
var testenv *envtest.Environment
var cfg *rest.Config
var clientset *kubernetes.Clientset
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(logf.ZapLoggerTo(GinkgoWriter, true))
testenv = &envtest.Environment{}
var err error
cfg, err = testenv.Start()
Expect(err).NotTo(HaveOccurred())
clientset, err = kubernetes.NewForConfig(cfg)
Expect(err).NotTo(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
testenv.Stop()
})

View File

@ -0,0 +1,431 @@
/*
Copyright 2018 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 controller
import (
"fmt"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/cache/informertest"
"sigs.k8s.io/controller-runtime/pkg/controller/controllertest"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/reconcile/reconciletest"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var _ = Describe("controller", func() {
var fakeReconcile *reconciletest.FakeReconcile
var ctrl *Controller
var queue *controllertest.Queue
var informers *informertest.FakeInformers
var stop chan struct{}
var reconciled chan reconcile.Request
var request = reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"},
}
BeforeEach(func() {
stop = make(chan struct{})
reconciled = make(chan reconcile.Request)
fakeReconcile = &reconciletest.FakeReconcile{
Chan: reconciled,
}
queue = &controllertest.Queue{
Interface: workqueue.New(),
}
informers = &informertest.FakeInformers{}
ctrl = &Controller{
MaxConcurrentReconciles: 1,
Do: fakeReconcile,
Queue: queue,
Cache: informers,
}
ctrl.InjectFunc(func(interface{}) error { return nil })
})
AfterEach(func() {
close(stop)
})
Describe("Reconciler", func() {
It("should call the Reconciler function", func() {
ctrl.Do = reconcile.Func(func(reconcile.Request) (reconcile.Result, error) {
return reconcile.Result{Requeue: true}, nil
})
result, err := ctrl.Reconcile(
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(reconcile.Result{Requeue: true}))
})
})
Describe("Start", func() {
It("should return an error if there is an error waiting for the informers", func(done Done) {
ctrl.WaitForCacheSync = func(<-chan struct{}) bool { return false }
ctrl.Name = "foo"
err := ctrl.Start(stop)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to wait for foo caches to sync"))
close(done)
})
It("should wait for each informer to sync", func(done Done) {
// Use a stopped channel so Start doesn't block
stopped := make(chan struct{})
close(stopped)
c, err := cache.New(cfg, cache.Options{})
Expect(err).NotTo(HaveOccurred())
c.GetInformer(&v1.Deployment{})
c.GetInformer(&v1.ReplicaSet{})
ctrl.Cache = c
ctrl.WaitForCacheSync = func(<-chan struct{}) bool { return true }
Expect(ctrl.Start(stopped)).NotTo(HaveOccurred())
close(done)
})
})
Describe("Watch", func() {
It("should inject dependencies into the Source", func() {
src := &source.Kind{Type: &corev1.Pod{}}
src.InjectCache(ctrl.Cache)
evthdl := &handler.EnqueueRequestForObject{}
found := false
ctrl.SetFields = func(i interface{}) error {
defer GinkgoRecover()
if i == src {
found = true
}
return nil
}
Expect(ctrl.Watch(src, evthdl)).NotTo(HaveOccurred())
Expect(found).To(BeTrue(), "Source not injected")
})
It("should return an error if there is an error injecting into the Source", func() {
src := &source.Kind{Type: &corev1.Pod{}}
src.InjectCache(ctrl.Cache)
evthdl := &handler.EnqueueRequestForObject{}
expected := fmt.Errorf("expect fail source")
ctrl.SetFields = func(i interface{}) error {
defer GinkgoRecover()
if i == src {
return expected
}
return nil
}
Expect(ctrl.Watch(src, evthdl)).To(Equal(expected))
})
It("should inject dependencies into the EventHandler", func() {
src := &source.Kind{Type: &corev1.Pod{}}
src.InjectCache(ctrl.Cache)
evthdl := &handler.EnqueueRequestForObject{}
found := false
ctrl.SetFields = func(i interface{}) error {
defer GinkgoRecover()
if i == evthdl {
found = true
}
return nil
}
Expect(ctrl.Watch(src, evthdl)).NotTo(HaveOccurred())
Expect(found).To(BeTrue(), "EventHandler not injected")
})
It("should return an error if there is an error injecting into the EventHandler", func() {
src := &source.Kind{Type: &corev1.Pod{}}
evthdl := &handler.EnqueueRequestForObject{}
expected := fmt.Errorf("expect fail eventhandler")
ctrl.SetFields = func(i interface{}) error {
defer GinkgoRecover()
if i == evthdl {
return expected
}
return nil
}
Expect(ctrl.Watch(src, evthdl)).To(Equal(expected))
})
It("should inject dependencies into the Reconciler", func() {
// TODO(community): Write this
})
It("should return an error if there is an error injecting into the Reconciler", func() {
// TODO(community): Write this
})
It("should inject dependencies into all of the Predicates", func() {
src := &source.Kind{Type: &corev1.Pod{}}
src.InjectCache(ctrl.Cache)
evthdl := &handler.EnqueueRequestForObject{}
pr1 := &predicate.Funcs{}
pr2 := &predicate.Funcs{}
found1 := false
found2 := false
ctrl.SetFields = func(i interface{}) error {
defer GinkgoRecover()
if i == pr1 {
found1 = true
}
if i == pr2 {
found2 = true
}
return nil
}
Expect(ctrl.Watch(src, evthdl, pr1, pr2)).NotTo(HaveOccurred())
Expect(found1).To(BeTrue(), "First Predicated not injected")
Expect(found2).To(BeTrue(), "Second Predicated not injected")
})
It("should return an error if there is an error injecting into any of the Predicates", func() {
src := &source.Kind{Type: &corev1.Pod{}}
src.InjectCache(ctrl.Cache)
evthdl := &handler.EnqueueRequestForObject{}
pr1 := &predicate.Funcs{}
pr2 := &predicate.Funcs{}
expected := fmt.Errorf("expect fail predicate")
ctrl.SetFields = func(i interface{}) error {
defer GinkgoRecover()
if i == pr1 {
return expected
}
return nil
}
Expect(ctrl.Watch(src, evthdl, pr1, pr2)).To(Equal(expected))
ctrl.SetFields = func(i interface{}) error {
defer GinkgoRecover()
if i == pr2 {
return expected
}
return nil
}
Expect(ctrl.Watch(src, evthdl, pr1, pr2)).To(Equal(expected))
})
It("should call Start the Source with the EventHandler, Queue, and Predicates", func() {
pr1 := &predicate.Funcs{}
pr2 := &predicate.Funcs{}
evthdl := &handler.EnqueueRequestForObject{}
src := source.Func(func(e handler.EventHandler, q workqueue.RateLimitingInterface, p ...predicate.Predicate) error {
defer GinkgoRecover()
Expect(e).To(Equal(evthdl))
Expect(q).To(Equal(ctrl.Queue))
Expect(p).To(ConsistOf(pr1, pr2))
return nil
})
Expect(ctrl.Watch(src, evthdl, pr1, pr2)).NotTo(HaveOccurred())
})
It("should return an error if there is an error starting the Source", func() {
err := fmt.Errorf("Expected Error: could not start source")
src := source.Func(func(handler.EventHandler,
workqueue.RateLimitingInterface,
...predicate.Predicate) error {
defer GinkgoRecover()
return err
})
Expect(ctrl.Watch(src, &handler.EnqueueRequestForObject{})).To(Equal(err))
})
})
Describe("Processing queue items from a Controller", func() {
It("should call Reconciler if an item is enqueued", func(done Done) {
go func() {
defer GinkgoRecover()
Expect(ctrl.Start(stop)).NotTo(HaveOccurred())
}()
ctrl.Queue.Add(request)
By("Invoking Reconciler")
Expect(<-reconciled).To(Equal(request))
By("Removing the item from the queue")
Eventually(ctrl.Queue.Len).Should(Equal(0))
Eventually(func() int { return ctrl.Queue.NumRequeues(request) }).Should(Equal(0))
close(done)
})
It("should continue to process additional queue items after the first", func(done Done) {
ctrl.Do = reconcile.Func(func(reconcile.Request) (reconcile.Result, error) {
defer GinkgoRecover()
Fail("Reconciler should not have been called")
return reconcile.Result{}, nil
})
go func() {
defer GinkgoRecover()
Expect(ctrl.Start(stop)).NotTo(HaveOccurred())
}()
ctrl.Queue.Add("foo/bar")
// Don't expect the string to reconciled
Expect(ctrl.processNextWorkItem()).To(BeTrue())
Eventually(ctrl.Queue.Len).Should(Equal(0))
Eventually(func() int { return ctrl.Queue.NumRequeues(request) }).Should(Equal(0))
close(done)
})
It("should forget an item if it is not a Request and continue processing items", func() {
// TODO(community): write this test
})
It("should requeue a Request if there is an error and continue processing items", func(done Done) {
fakeReconcile.Err = fmt.Errorf("expected error: reconcile")
go func() {
defer GinkgoRecover()
Expect(ctrl.Start(stop)).NotTo(HaveOccurred())
}()
ctrl.Queue.Add(request)
// Reduce the jitterperiod so we don't have to wait a second before the reconcile function is rerun.
ctrl.JitterPeriod = time.Millisecond
By("Invoking Reconciler which will give an error")
Expect(<-reconciled).To(Equal(request))
By("Invoking Reconciler a second time without error")
fakeReconcile.Err = nil
Expect(<-reconciled).To(Equal(request))
By("Removing the item from the queue")
Eventually(ctrl.Queue.Len).Should(Equal(0))
Eventually(func() int { return ctrl.Queue.NumRequeues(request) }).Should(Equal(0))
close(done)
}, 1.0)
It("should requeue a Request if the Result sets Requeue:true and continue processing items", func() {
fakeReconcile.Result.Requeue = true
go func() {
defer GinkgoRecover()
Expect(ctrl.Start(stop)).NotTo(HaveOccurred())
}()
dq := &DelegatingQueue{RateLimitingInterface: ctrl.Queue}
ctrl.Queue = dq
ctrl.Queue.Add(request)
Expect(dq.countAdd).To(Equal(1))
Expect(dq.countAddAfter).To(Equal(0))
Expect(dq.countAddRateLimited).To(Equal(0))
By("Invoking Reconciler which will ask for requeue")
Expect(<-reconciled).To(Equal(request))
Expect(dq.countAdd).To(Equal(1))
Expect(dq.countAddAfter).To(Equal(0))
Expect(dq.countAddRateLimited).To(Equal(1))
By("Invoking Reconciler a second time without asking for requeue")
fakeReconcile.Result.Requeue = false
Expect(<-reconciled).To(Equal(request))
Expect(dq.countAdd).To(Equal(1))
Expect(dq.countAddAfter).To(Equal(0))
Expect(dq.countAddRateLimited).To(Equal(1))
By("Removing the item from the queue")
Eventually(ctrl.Queue.Len).Should(Equal(0))
Eventually(func() int { return ctrl.Queue.NumRequeues(request) }).Should(Equal(0))
})
It("should requeue a Request after a duration if the Result sets Requeue:true and "+
"RequeueAfter is set", func() {
fakeReconcile.Result.RequeueAfter = time.Millisecond * 100
go func() {
defer GinkgoRecover()
Expect(ctrl.Start(stop)).NotTo(HaveOccurred())
}()
dq := &DelegatingQueue{RateLimitingInterface: ctrl.Queue}
ctrl.Queue = dq
ctrl.Queue.Add(request)
Expect(dq.countAdd).To(Equal(1))
Expect(dq.countAddAfter).To(Equal(0))
Expect(dq.countAddRateLimited).To(Equal(0))
By("Invoking Reconciler which will ask for requeue")
Expect(<-reconciled).To(Equal(request))
Expect(dq.countAdd).To(Equal(1))
Expect(dq.countAddAfter).To(Equal(1))
Expect(dq.countAddRateLimited).To(Equal(0))
By("Invoking Reconciler a second time without asking for requeue")
fakeReconcile.Result.Requeue = false
Expect(<-reconciled).To(Equal(request))
Expect(dq.countAdd).To(Equal(1))
Expect(dq.countAddAfter).To(Equal(1))
Expect(dq.countAddRateLimited).To(Equal(0))
By("Removing the item from the queue")
Eventually(ctrl.Queue.Len).Should(Equal(0))
Eventually(func() int { return ctrl.Queue.NumRequeues(request) }).Should(Equal(0))
})
It("should forget the Request if Reconciler is successful", func() {
// TODO(community): write this test
})
It("should return if the queue is shutdown", func() {
// TODO(community): write this test
})
It("should wait for informers to be synced before processing items", func() {
// TODO(community): write this test
})
It("should create a new go routine for MaxConcurrentReconciles", func() {
// TODO(community): write this test
})
})
})
type DelegatingQueue struct {
workqueue.RateLimitingInterface
countAddRateLimited int
countAdd int
countAddAfter int
}
func (q *DelegatingQueue) AddRateLimited(item interface{}) {
q.countAddRateLimited++
q.RateLimitingInterface.AddRateLimited(item)
}
func (q *DelegatingQueue) AddAfter(item interface{}, d time.Duration) {
q.countAddAfter++
q.RateLimitingInterface.AddAfter(item, d)
}
func (q *DelegatingQueue) Add(item interface{}) {
q.countAdd++
q.RateLimitingInterface.Add(item)
}

View File

@ -0,0 +1,61 @@
/*
Copyright 2018 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 recorder
import (
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/recorder"
)
type provider struct {
// scheme to specify when creating a recorder
scheme *runtime.Scheme
// eventBroadcaster to create new recorder instance
eventBroadcaster record.EventBroadcaster
// logger is the logger to use when logging diagnostic event info
logger logr.Logger
}
// NewProvider create a new Provider instance.
func NewProvider(config *rest.Config, scheme *runtime.Scheme, logger logr.Logger) (recorder.Provider, error) {
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to init clientSet: %v", err)
}
p := &provider{scheme: scheme, logger: logger}
p.eventBroadcaster = record.NewBroadcaster()
p.eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: clientSet.CoreV1().Events("")})
p.eventBroadcaster.StartEventWatcher(
func(e *corev1.Event) {
p.logger.V(1).Info(e.Type, "object", e.InvolvedObject, "reason", e.Reason, "message", e.Message)
})
return p, nil
}
func (p *provider) GetEventRecorderFor(name string) record.EventRecorder {
return p.eventBroadcaster.NewRecorder(p.scheme, corev1.EventSource{Component: name})
}

View File

@ -0,0 +1,122 @@
/*
Copyright 2018 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 recorder_test
import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/scheme"
ref "k8s.io/client-go/tools/reference"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("recorder", func() {
var stop chan struct{}
BeforeEach(func() {
stop = make(chan struct{})
Expect(cfg).NotTo(BeNil())
})
AfterEach(func() {
close(stop)
})
Describe("recorder", func() {
It("should publish events", func(done Done) {
By("Creating the Manager")
cm, err := manager.New(cfg, manager.Options{})
Expect(err).NotTo(HaveOccurred())
By("Creating the Controller")
recorder := cm.GetRecorder("test-recorder")
instance, err := controller.New("foo-controller", cm, controller.Options{
Reconciler: reconcile.Func(
func(request reconcile.Request) (reconcile.Result, error) {
dp, err := clientset.AppsV1().Deployments(request.Namespace).Get(request.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
recorder.Event(dp, corev1.EventTypeNormal, "test-reason", "test-msg")
return reconcile.Result{}, nil
}),
})
Expect(err).NotTo(HaveOccurred())
By("Watching Resources")
err = instance.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{})
Expect(err).NotTo(HaveOccurred())
By("Starting the Manager")
go func() {
defer GinkgoRecover()
Expect(cm.Start(stop)).NotTo(HaveOccurred())
}()
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "deployment-name"},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
},
},
}
By("Invoking Reconciling")
deployment, err = clientset.AppsV1().Deployments("default").Create(deployment)
Expect(err).NotTo(HaveOccurred())
By("Validate event is published as expected")
evtWatcher, err := clientset.CoreV1().Events("default").Watch(metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred())
resultEvent := <-evtWatcher.ResultChan()
Expect(resultEvent.Type).To(Equal(watch.Added))
evt, isEvent := resultEvent.Object.(*corev1.Event)
Expect(isEvent).To(BeTrue())
dpRef, err := ref.GetReference(scheme.Scheme, deployment)
Expect(err).NotTo(HaveOccurred())
Expect(evt.InvolvedObject).To(Equal(*dpRef))
Expect(evt.Type).To(Equal(corev1.EventTypeNormal))
Expect(evt.Reason).To(Equal("test-reason"))
Expect(evt.Message).To(Equal("test-msg"))
close(done)
})
})
})

Some files were not shown because too many files have changed in this diff Show More