diff --git a/alias.go b/alias.go index b41f51435b..af955ad301 100644 --- a/alias.go +++ b/alias.go @@ -94,6 +94,9 @@ var ( // NewControllerManagedBy returns a new controller builder that will be started by the provided Manager NewControllerManagedBy = builder.ControllerManagedBy + // NewWebhookManagedBy returns a new webhook builder that will be started by the provided Manager + NewWebhookManagedBy = builder.WebhookManagedBy + // NewManager returns a new Manager for creating Controllers. NewManager = manager.New diff --git a/examples/crd/main.go b/examples/crd/main.go index 82dcd75c5d..86339a535e 100644 --- a/examples/crd/main.go +++ b/examples/crd/main.go @@ -125,12 +125,19 @@ func main() { Client: mgr.GetClient(), scheme: mgr.GetScheme(), }) - if err != nil { setupLog.Error(err, "unable to create controller") os.Exit(1) } + err = ctrl.NewWebhookManagedBy(mgr). + For(&api.ChaosPod{}). + Complete() + if err != nil { + setupLog.Error(err, "unable to create webhook") + os.Exit(1) + } + setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") diff --git a/pkg/builder/build.go b/pkg/builder/controller.go similarity index 69% rename from pkg/builder/build.go rename to pkg/builder/controller.go index 5bb70e8ceb..d89f3696ff 100644 --- a/pkg/builder/build.go +++ b/pkg/builder/controller.go @@ -18,12 +18,9 @@ package builder import ( "fmt" - "net/http" - "net/url" "strings" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -33,8 +30,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" ) // Supporting mocking out functions for testing @@ -71,8 +66,6 @@ func ControllerManagedBy(m manager.Manager) *Builder { // update events by *reconciling the object*. // This is the equivalent of calling // Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}) -// If the passed in object has implemented the admission.Defaulter interface, a MutatingWebhook will be wired for this type. -// If the passed in object has implemented the admission.Validator interface, a ValidatingWebhook will be wired for this type. // // Deprecated: Use For func (blder *Builder) ForType(apiType runtime.Object) *Builder { @@ -83,8 +76,6 @@ func (blder *Builder) ForType(apiType runtime.Object) *Builder { // update events by *reconciling the object*. // This is the equivalent of calling // Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}) -// If the passed in object has implemented the admission.Defaulter interface, a MutatingWebhook will be wired for this type. -// If the passed in object has implemented the admission.Validator interface, a ValidatingWebhook will be wired for this type. func (blder *Builder) For(apiType runtime.Object) *Builder { blder.apiType = apiType return blder @@ -159,7 +150,7 @@ func (blder *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) { } // Set the Config - if err := blder.doConfig(); err != nil { + if err := blder.loadRestConfig(); err != nil { return nil, err } @@ -173,11 +164,6 @@ func (blder *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) { return nil, err } - // Set the Webook if needed - if err := blder.doWebhook(); err != nil { - return nil, err - } - // Set the Watch if err := blder.doWatch(); err != nil { return nil, err @@ -217,7 +203,7 @@ func (blder *Builder) doWatch() error { return nil } -func (blder *Builder) doConfig() error { +func (blder *Builder) loadRestConfig() error { if blder.config != nil { return nil } @@ -258,79 +244,3 @@ func (blder *Builder) doController(r reconcile.Reconciler) error { blder.ctrl, err = newController(name, blder.mgr, controller.Options{Reconciler: r}) return err } - -func (blder *Builder) doWebhook() error { - // Create a webhook for each type - gvk, err := apiutil.GVKForObject(blder.apiType, blder.mgr.GetScheme()) - if err != nil { - return err - } - - // TODO: When the conversion webhook lands, we need to handle all registered versions of a given group-kind. - // A potential workflow for defaulting webhook - // 1) a bespoke (non-hub) version comes in - // 2) convert it to the hub version - // 3) do defaulting - // 4) convert it back to the same bespoke version - // 5) calculate the JSON patch - // - // A potential workflow for validating webhook - // 1) a bespoke (non-hub) version comes in - // 2) convert it to the hub version - // 3) do validation - if defaulter, isDefaulter := blder.apiType.(admission.Defaulter); isDefaulter { - mwh := admission.DefaultingWebhookFor(defaulter) - if mwh != nil { - path := generateMutatePath(gvk) - - // Checking if the path is already registered. - // If so, just skip it. - if !blder.isAlreadyHandled(path) { - log.Info("Registering a mutating webhook", - "GVK", gvk, - "path", path) - blder.mgr.GetWebhookServer().Register(path, mwh) - } - } - } - - if validator, isValidator := blder.apiType.(admission.Validator); isValidator { - vwh := admission.ValidatingWebhookFor(validator) - if vwh != nil { - path := generateValidatePath(gvk) - - // Checking if the path is already registered. - // If so, just skip it. - if !blder.isAlreadyHandled(path) { - log.Info("Registering a validating webhook", - "GVK", gvk, - "path", path) - blder.mgr.GetWebhookServer().Register(path, vwh) - } - } - } - - err = conversion.CheckConvertibility(blder.mgr.GetScheme(), blder.apiType) - if err != nil { - log.Error(err, "conversion check failed", "GVK", gvk) - } - return nil -} - -func (blder *Builder) isAlreadyHandled(path string) bool { - h, p := blder.mgr.GetWebhookServer().WebhookMux.Handler(&http.Request{URL: &url.URL{Path: path}}) - if p == path && h != nil { - return true - } - return false -} - -func generateMutatePath(gvk schema.GroupVersionKind) string { - return "/mutate-" + strings.Replace(gvk.Group, ".", "-", -1) + "-" + - gvk.Version + "-" + strings.ToLower(gvk.Kind) -} - -func generateValidatePath(gvk schema.GroupVersionKind) string { - return "/validate-" + strings.Replace(gvk.Group, ".", "-", -1) + "-" + - gvk.Version + "-" + strings.ToLower(gvk.Kind) -} diff --git a/pkg/builder/controller_test.go b/pkg/builder/controller_test.go new file mode 100644 index 0000000000..29917edbe5 --- /dev/null +++ b/pkg/builder/controller_test.go @@ -0,0 +1,310 @@ +/* +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" + "strings" + + . "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/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/scheme" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +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 + }) + + 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(). + For(&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(). + For(&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(). + For(&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(). + For(&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(). + For(&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(). + For(&appsv1.ReplicaSet{}). + Owns(&appsv1.ReplicaSet{}). + Build(noop) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expected error")) + Expect(instance).To(BeNil()) + }) + + It("should allow multiple controllers for the same kind", func() { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaultValidatorGVK.GroupVersion()} + builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + Expect(err).NotTo(HaveOccurred()) + + By("creating the 1st controller") + ctrl1, err := ControllerManagedBy(m). + For(&TestDefaultValidator{}). + Owns(&appsv1.ReplicaSet{}). + Build(noop) + Expect(err).NotTo(HaveOccurred()) + Expect(ctrl1).NotTo(BeNil()) + + By("creating the 2nd controller") + ctrl2, err := ControllerManagedBy(m). + For(&TestDefaultValidator{}). + Owns(&appsv1.ReplicaSet{}). + Build(noop) + Expect(err).NotTo(HaveOccurred()) + Expect(ctrl2).NotTo(BeNil()) + }) + }) + + Describe("Start with SimpleController", func() { + It("should Reconcile Owns objects", func(done Done) { + bldr := SimpleController(). + ForType(&appsv1.Deployment{}). + WithConfig(cfg). + Owns(&appsv1.ReplicaSet{}) + doReconcileTest("1", stop, bldr, nil, false) + + close(done) + }, 10) + + It("should Reconcile Owns objects with a Manager", func(done Done) { + m, err := manager.New(cfg, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + + bldr := SimpleController(). + WithManager(m). + For(&appsv1.Deployment{}). + Owns(&appsv1.ReplicaSet{}) + doReconcileTest("2", stop, bldr, m, false) + close(done) + }, 10) + }) + + Describe("Start with ControllerManagedBy", func() { + It("should Reconcile Owns objects", func(done Done) { + m, err := manager.New(cfg, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + + bldr := ControllerManagedBy(m). + For(&appsv1.Deployment{}). + Owns(&appsv1.ReplicaSet{}) + doReconcileTest("3", stop, bldr, m, false) + close(done) + }, 10) + + It("should Reconcile Watches objects", func(done Done) { + m, err := manager.New(cfg, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + + bldr := ControllerManagedBy(m). + For(&appsv1.Deployment{}). + Watches( // Equivalent of Owns + &source.Kind{Type: &appsv1.ReplicaSet{}}, + &handler.EnqueueRequestForOwner{OwnerType: &appsv1.Deployment{}, IsController: true}) + doReconcileTest("4", stop, bldr, m, true) + close(done) + }, 10) + }) +}) + +func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr manager.Manager, complete bool) { + deployName := "deploy-name-" + nameSuffix + rsName := "rs-name-" + nameSuffix + + By("Creating the application") + ch := make(chan reconcile.Request) + fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { + defer GinkgoRecover() + if !strings.HasSuffix(req.Name, nameSuffix) { + // From different test, ignore this request. Etcd is shared across tests. + return reconcile.Result{}, nil + } + ch <- req + return reconcile.Result{}, nil + }) + + instance := mgr + if complete { + err := blder.Complete(fn) + Expect(err).NotTo(HaveOccurred()) + } else { + var err error + instance, err = blder.Build(fn) + Expect(err).NotTo(HaveOccurred()) + } + + // Manager should match + if mgr != nil { + Expect(instance).To(Equal(mgr)) + } + + 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: deployName, + }, + 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: deployName}})) + + By("Creating a ReplicaSet") + // Expect a Reconcile when an Owned object is managedObjects. + t := true + rs := &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: rsName, + Labels: dep.Spec.Selector.MatchLabels, + OwnerReferences: []metav1.OwnerReference{ + { + Name: deployName, + 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: deployName}})) + +} + +var _ runtime.Object = &fakeType{} + +type fakeType struct{} + +func (*fakeType) GetObjectKind() schema.ObjectKind { return nil } +func (*fakeType) DeepCopyObject() runtime.Object { return nil } diff --git a/pkg/builder/example_webhook_test.go b/pkg/builder/example_webhook_test.go new file mode 100644 index 0000000000..63333a2478 --- /dev/null +++ b/pkg/builder/example_webhook_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2019 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 ( + "os" + + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client/config" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/manager/signals" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + examplegroup "sigs.k8s.io/controller-runtime/examples/crd/pkg" +) + +// examplegroup.ChaosPod has implemented both admission.Defaulter and +// admission.Validator interfaces. +var _ admission.Defaulter = &examplegroup.ChaosPod{} +var _ admission.Validator = &examplegroup.ChaosPod{} + +// This example use webhook builder to create a simple webhook that is managed +// by a manager for CRD ChaosPod. And then start the manager. +func ExampleWebhookBuilder() { + var log = logf.Log.WithName("webhookbuilder-example") + + mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{}) + if err != nil { + log.Error(err, "could not create manager") + os.Exit(1) + } + + err = builder. + WebhookManagedBy(mgr). // Create the WebhookManagedBy + For(&examplegroup.ChaosPod{}). // ChaosPod is a CRD. + Complete() + if err != nil { + log.Error(err, "could not create webhook") + os.Exit(1) + } + + if err := mgr.Start(signals.SetupSignalHandler()); err != nil { + log.Error(err, "could not start manager") + os.Exit(1) + } +} diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go new file mode 100644 index 0000000000..d807552a02 --- /dev/null +++ b/pkg/builder/webhook.go @@ -0,0 +1,150 @@ +/* +Copyright 2019 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 ( + "net/http" + "net/url" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" +) + +// WebhookBuilder builds a Webhook. +type WebhookBuilder struct { + apiType runtime.Object + gvk schema.GroupVersionKind + mgr manager.Manager + config *rest.Config +} + +func WebhookManagedBy(m manager.Manager) *WebhookBuilder { + return &WebhookBuilder{mgr: m} +} + +// TODO(droot): update the GoDoc for conversion. + +// For takes a runtime.Object which should be a CR. +// If the given object implements the admission.Defaulter interface, a MutatingWebhook will be wired for this type. +// If the given object implements the admission.Validator interface, a ValidatingWebhook will be wired for this type. +func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder { + blder.apiType = apiType + return blder +} + +// Complete builds the webhook. +func (blder *WebhookBuilder) Complete() error { + // Set the Config + if err := blder.loadRestConfig(); err != nil { + return err + } + + // Set the Webook if needed + return blder.registerWebhooks() +} + +func (blder *WebhookBuilder) loadRestConfig() error { + if blder.config != nil { + return nil + } + if blder.mgr != nil { + blder.config = blder.mgr.GetConfig() + return nil + } + var err error + blder.config, err = getConfig() + return err +} + +func (blder *WebhookBuilder) registerWebhooks() error { + // Create webhook(s) for each type + var err error + blder.gvk, err = apiutil.GVKForObject(blder.apiType, blder.mgr.GetScheme()) + if err != nil { + return err + } + + blder.registerDefaultingWebhook() + blder.registerValidatingWebhook() + + err = conversion.CheckConvertibility(blder.mgr.GetScheme(), blder.apiType) + if err != nil { + log.Error(err, "conversion check failed", "GVK", blder.gvk) + } + return nil +} + +// registerDefaultingWebhook registers a defaulting webhook if th +func (blder *WebhookBuilder) registerDefaultingWebhook() { + if defaulter, isDefaulter := blder.apiType.(admission.Defaulter); isDefaulter { + mwh := admission.DefaultingWebhookFor(defaulter) + if mwh != nil { + path := generateMutatePath(blder.gvk) + + // Checking if the path is already registered. + // If so, just skip it. + if !blder.isAlreadyHandled(path) { + log.Info("Registering a mutating webhook", + "GVK", blder.gvk, + "path", path) + blder.mgr.GetWebhookServer().Register(path, mwh) + } + } + } +} + +func (blder *WebhookBuilder) registerValidatingWebhook() { + if validator, isValidator := blder.apiType.(admission.Validator); isValidator { + vwh := admission.ValidatingWebhookFor(validator) + if vwh != nil { + path := generateValidatePath(blder.gvk) + + // Checking if the path is already registered. + // If so, just skip it. + if !blder.isAlreadyHandled(path) { + log.Info("Registering a validating webhook", + "GVK", blder.gvk, + "path", path) + blder.mgr.GetWebhookServer().Register(path, vwh) + } + } + } +} + +func (blder *WebhookBuilder) isAlreadyHandled(path string) bool { + h, p := blder.mgr.GetWebhookServer().WebhookMux.Handler(&http.Request{URL: &url.URL{Path: path}}) + if p == path && h != nil { + return true + } + return false +} + +func generateMutatePath(gvk schema.GroupVersionKind) string { + return "/mutate-" + strings.Replace(gvk.Group, ".", "-", -1) + "-" + + gvk.Version + "-" + strings.ToLower(gvk.Kind) +} + +func generateValidatePath(gvk schema.GroupVersionKind) string { + return "/validate-" + strings.Replace(gvk.Group, ".", "-", -1) + "-" + + gvk.Version + "-" + strings.ToLower(gvk.Kind) +} diff --git a/pkg/builder/build_test.go b/pkg/builder/webhook_test.go similarity index 58% rename from pkg/builder/build_test.go rename to pkg/builder/webhook_test.go index 545f222a9e..9554ecbced 100644 --- a/pkg/builder/build_test.go +++ b/pkg/builder/webhook_test.go @@ -1,5 +1,5 @@ /* -Copyright 2018 The Kubernetes Authors. +Copyright 2019 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. @@ -17,7 +17,6 @@ limitations under the License. package builder import ( - "context" "errors" "fmt" "net/http" @@ -27,19 +26,12 @@ 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/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "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/scheme" - "sigs.k8s.io/controller-runtime/pkg/source" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -57,74 +49,7 @@ var _ = Describe("application", 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(). - For(&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(). - For(&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(). - For(&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(). - For(&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(). - For(&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(). - For(&appsv1.ReplicaSet{}). - Owns(&appsv1.ReplicaSet{}). - Build(noop) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("expected error")) - Expect(instance).To(BeNil()) - }) - It("should scaffold a defaulting webhook if the type implements the Defaulter interface", func() { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) @@ -136,12 +61,10 @@ var _ = Describe("application", func() { err = builder.AddToScheme(m.GetScheme()) Expect(err).NotTo(HaveOccurred()) - instance, err := ControllerManagedBy(m). + err = WebhookManagedBy(m). For(&TestDefaulter{}). - Owns(&appsv1.ReplicaSet{}). - Build(noop) + Complete() Expect(err).NotTo(HaveOccurred()) - Expect(instance).NotTo(BeNil()) svr := m.GetWebhookServer() Expect(svr).NotTo(BeNil()) @@ -210,12 +133,10 @@ var _ = Describe("application", func() { err = builder.AddToScheme(m.GetScheme()) Expect(err).NotTo(HaveOccurred()) - instance, err := ControllerManagedBy(m). + err = WebhookManagedBy(m). For(&TestValidator{}). - Owns(&appsv1.ReplicaSet{}). - Build(noop) + Complete() Expect(err).NotTo(HaveOccurred()) - Expect(instance).NotTo(BeNil()) svr := m.GetWebhookServer() Expect(svr).NotTo(BeNil()) @@ -285,12 +206,10 @@ var _ = Describe("application", func() { err = builder.AddToScheme(m.GetScheme()) Expect(err).NotTo(HaveOccurred()) - instance, err := ControllerManagedBy(m). + err = WebhookManagedBy(m). For(&TestDefaultValidator{}). - Owns(&appsv1.ReplicaSet{}). - Build(noop) + Complete() Expect(err).NotTo(HaveOccurred()) - Expect(instance).NotTo(BeNil()) svr := m.GetWebhookServer() Expect(svr).NotTo(BeNil()) @@ -350,195 +269,9 @@ var _ = Describe("application", func() { Expect(w.Body).To(ContainSubstring(`"allowed":true`)) Expect(w.Body).To(ContainSubstring(`"code":200`)) }) - - It("should allow multiple controllers for the same kind", func() { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - Expect(err).NotTo(HaveOccurred()) - - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaultValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - Expect(err).NotTo(HaveOccurred()) - - By("creating the 1st controller") - ctrl1, err := ControllerManagedBy(m). - For(&TestDefaultValidator{}). - Owns(&appsv1.ReplicaSet{}). - Build(noop) - Expect(err).NotTo(HaveOccurred()) - Expect(ctrl1).NotTo(BeNil()) - - By("creating the 2nd controller") - ctrl2, err := ControllerManagedBy(m). - For(&TestDefaultValidator{}). - Owns(&appsv1.ReplicaSet{}). - Build(noop) - Expect(err).NotTo(HaveOccurred()) - Expect(ctrl2).NotTo(BeNil()) - }) - }) - - Describe("Start with SimpleController", func() { - It("should Reconcile Owns objects", func(done Done) { - bldr := SimpleController(). - ForType(&appsv1.Deployment{}). - WithConfig(cfg). - Owns(&appsv1.ReplicaSet{}) - doReconcileTest("1", stop, bldr, nil, false) - - close(done) - }, 10) - - It("should Reconcile Owns objects with a Manager", func(done Done) { - m, err := manager.New(cfg, manager.Options{}) - Expect(err).NotTo(HaveOccurred()) - - bldr := SimpleController(). - WithManager(m). - For(&appsv1.Deployment{}). - Owns(&appsv1.ReplicaSet{}) - doReconcileTest("2", stop, bldr, m, false) - close(done) - }, 10) - }) - - Describe("Start with ControllerManagedBy", func() { - It("should Reconcile Owns objects", func(done Done) { - m, err := manager.New(cfg, manager.Options{}) - Expect(err).NotTo(HaveOccurred()) - - bldr := ControllerManagedBy(m). - For(&appsv1.Deployment{}). - Owns(&appsv1.ReplicaSet{}) - doReconcileTest("3", stop, bldr, m, false) - close(done) - }, 10) - - It("should Reconcile Watches objects", func(done Done) { - m, err := manager.New(cfg, manager.Options{}) - Expect(err).NotTo(HaveOccurred()) - - bldr := ControllerManagedBy(m). - For(&appsv1.Deployment{}). - Watches( // Equivalent of Owns - &source.Kind{Type: &appsv1.ReplicaSet{}}, - &handler.EnqueueRequestForOwner{OwnerType: &appsv1.Deployment{}, IsController: true}) - doReconcileTest("4", stop, bldr, m, true) - close(done) - }, 10) }) }) -func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr manager.Manager, complete bool) { - deployName := "deploy-name-" + nameSuffix - rsName := "rs-name-" + nameSuffix - - By("Creating the application") - ch := make(chan reconcile.Request) - fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { - defer GinkgoRecover() - if !strings.HasSuffix(req.Name, nameSuffix) { - // From different test, ignore this request. Etcd is shared across tests. - return reconcile.Result{}, nil - } - ch <- req - return reconcile.Result{}, nil - }) - - instance := mgr - if complete { - err := blder.Complete(fn) - Expect(err).NotTo(HaveOccurred()) - } else { - var err error - instance, err = blder.Build(fn) - Expect(err).NotTo(HaveOccurred()) - } - - // Manager should match - if mgr != nil { - Expect(instance).To(Equal(mgr)) - } - - 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: deployName, - }, - 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: deployName}})) - - By("Creating a ReplicaSet") - // Expect a Reconcile when an Owned object is managedObjects. - t := true - rs := &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: rsName, - Labels: dep.Spec.Selector.MatchLabels, - OwnerReferences: []metav1.OwnerReference{ - { - Name: deployName, - 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: deployName}})) - -} - -var _ runtime.Object = &fakeType{} - -type fakeType struct{} - -func (*fakeType) GetObjectKind() schema.ObjectKind { return nil } -func (*fakeType) DeepCopyObject() runtime.Object { return nil } - // TestDefaulter var _ runtime.Object = &TestDefaulter{}