diff --git a/.golangci.yml b/.golangci.yml index 44a915409d..97d52e4ea4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,7 +28,6 @@ linters: - unparam - ineffassign - nakedret - - interfacer - gocyclo - lll - dupl diff --git a/Makefile b/Makefile index 823158ee74..9d17dbb583 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ TOOLS_DIR := hack/tools TOOLS_BIN_DIR := $(TOOLS_DIR)/bin GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/golangci-lint) GO_APIDIFF := $(TOOLS_BIN_DIR)/go-apidiff +CONTROLLER_GEN := $(TOOLS_BIN_DIR)/controller-gen # The help will print out all targets with their descriptions organized bellow their categories. The categories are represented by `##@` and the target descriptions by `##`. # The awk commands is responsible to read the entire set of makefiles included in this invocation, looking for lines of the file as xyz: ## something, and then pretty-format the target and help. Then, if there's a line with ##@ something, that gets pretty-printed as a category. @@ -66,6 +67,9 @@ $(GOLANGCI_LINT): $(TOOLS_DIR)/go.mod # Build golangci-lint from tools folder. $(GO_APIDIFF): $(TOOLS_DIR)/go.mod # Build go-apidiff from tools folder. cd $(TOOLS_DIR) && go build -tags=tools -o bin/go-apidiff github.com/joelanford/go-apidiff +$(CONTROLLER_GEN): $(TOOLS_DIR)/go.mod # Build controller-gen from tools folder. + cd $(TOOLS_DIR) && go build -tags=tools -o bin/controller-gen sigs.k8s.io/controller-tools/cmd/controller-gen + ## -------------------------------------- ## Linting ## -------------------------------------- @@ -83,6 +87,10 @@ modules: ## Runs go mod to ensure modules are up to date. go mod tidy cd $(TOOLS_DIR); go mod tidy +.PHONY: generate +generate: $(CONTROLLER_GEN) ## Runs controller-gen for internal types for config file + $(CONTROLLER_GEN) object paths="./pkg/config/v1alpha1/..." + ## -------------------------------------- ## Cleanup / Verification ## -------------------------------------- diff --git a/examples/configfile/builtin/config.yaml b/examples/configfile/builtin/config.yaml new file mode 100644 index 0000000000..7373775aae --- /dev/null +++ b/examples/configfile/builtin/config.yaml @@ -0,0 +1,6 @@ +apiServer: controller-runtime.sigs.k8s.io/v1alpha1 +kind: GenericControllerConfiguration +namespace: default +metricsBindAddress: :9091 +leaderElection: + leaderElect: false diff --git a/examples/configfile/builtin/controller.go b/examples/configfile/builtin/controller.go new file mode 100644 index 0000000000..8349bcd5aa --- /dev/null +++ b/examples/configfile/builtin/controller.go @@ -0,0 +1,74 @@ +/* +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" + + 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/log" + "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 +} + +// Implement reconcile.Reconciler so the controller can reconcile objects +var _ reconcile.Reconciler = &reconcileReplicaSet{} + +func (r *reconcileReplicaSet) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + // set up a convenient log object so we don't have to type request over and over again + log := log.FromContext(ctx) + + // 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 { + return reconcile.Result{}, fmt.Errorf("could not fetch ReplicaSet: %+v", 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 { + return reconcile.Result{}, fmt.Errorf("could not write ReplicaSet: %+v", err) + } + + return reconcile.Result{}, nil +} diff --git a/examples/configfile/builtin/main.go b/examples/configfile/builtin/main.go new file mode 100644 index 0000000000..99497a5748 --- /dev/null +++ b/examples/configfile/builtin/main.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "sigs.k8s.io/controller-runtime/pkg/client/config" + cfg "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/manager/signals" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var scheme = runtime.NewScheme() + +func init() { + log.SetLogger(zap.New()) + clientgoscheme.AddToScheme(scheme) +} + +func main() { + entryLog := log.Log.WithName("entrypoint") + + // Setup a Manager + entryLog.Info("setting up manager") + mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{ + Scheme: scheme, + }.AndFromOrDie(cfg.FromFile())) + if err != nil { + entryLog.Error(err, "unable to set up overall controller manager") + os.Exit(1) + } + + // Setup a new controller to reconcile ReplicaSets + entryLog.Info("Setting up controller") + c, err := controller.New("foo-controller", mgr, controller.Options{ + Reconciler: &reconcileReplicaSet{client: mgr.GetClient()}, + }) + 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) + } + + entryLog.Info("starting manager") + if err := mgr.Start(signals.SetupSignalHandler()); err != nil { + entryLog.Error(err, "unable to run manager") + os.Exit(1) + } +} diff --git a/examples/configfile/custom/config.yaml b/examples/configfile/custom/config.yaml new file mode 100644 index 0000000000..ffc81401a3 --- /dev/null +++ b/examples/configfile/custom/config.yaml @@ -0,0 +1,7 @@ +apiServer: examples.x-k8s.io/v1alpha1 +kind: CustomControllerConfiguration +clusterName: example-test +namespace: default +metricsBindAddress: :8081 +leaderElection: + leaderElect: false diff --git a/examples/configfile/custom/controller.go b/examples/configfile/custom/controller.go new file mode 100644 index 0000000000..8349bcd5aa --- /dev/null +++ b/examples/configfile/custom/controller.go @@ -0,0 +1,74 @@ +/* +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" + + 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/log" + "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 +} + +// Implement reconcile.Reconciler so the controller can reconcile objects +var _ reconcile.Reconciler = &reconcileReplicaSet{} + +func (r *reconcileReplicaSet) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + // set up a convenient log object so we don't have to type request over and over again + log := log.FromContext(ctx) + + // 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 { + return reconcile.Result{}, fmt.Errorf("could not fetch ReplicaSet: %+v", 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 { + return reconcile.Result{}, fmt.Errorf("could not write ReplicaSet: %+v", err) + } + + return reconcile.Result{}, nil +} diff --git a/examples/configfile/custom/main.go b/examples/configfile/custom/main.go new file mode 100644 index 0000000000..f9d1ccd2fc --- /dev/null +++ b/examples/configfile/custom/main.go @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "sigs.k8s.io/controller-runtime/examples/configfile/custom/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client/config" + cfg "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/manager/signals" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var scheme = runtime.NewScheme() + +func init() { + log.SetLogger(zap.New()) + clientgoscheme.AddToScheme(scheme) + v1alpha1.AddToScheme(scheme) +} + +func main() { + entryLog := log.Log.WithName("entrypoint") + + // Setup a Manager + entryLog.Info("setting up manager") + ctrlConfig := v1alpha1.CustomControllerConfiguration{} + + mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{ + Scheme: scheme, + }.AndFromOrDie(cfg.FromFile().OfKind(&ctrlConfig))) + if err != nil { + entryLog.Error(err, "unable to set up overall controller manager") + os.Exit(1) + } + + // Setup a new controller to reconcile ReplicaSets + entryLog.Info("Setting up controller in cluster", "name", ctrlConfig.ClusterName) + c, err := controller.New("foo-controller", mgr, controller.Options{ + Reconciler: &reconcileReplicaSet{client: mgr.GetClient()}, + }) + 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) + } + + entryLog.Info("starting manager") + if err := mgr.Start(signals.SetupSignalHandler()); err != nil { + entryLog.Error(err, "unable to run manager") + os.Exit(1) + } +} diff --git a/examples/configfile/custom/v1alpha1/types.go b/examples/configfile/custom/v1alpha1/types.go new file mode 100644 index 0000000000..3db3dcfc2a --- /dev/null +++ b/examples/configfile/custom/v1alpha1/types.go @@ -0,0 +1,54 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 provides the CustomControllerConfiguration used for +// demoing componentconfig +// +kubebuilder:object:generate=true +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + cfg "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "examples.x-k8s.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +// +kubebuilder:object:root=true + +// CustomControllerConfiguration is the Schema for the CustomControllerConfigurations API +type CustomControllerConfiguration struct { + metav1.TypeMeta `json:",inline"` + + // ControllerConfiguration returns the contfigurations for controllers + cfg.ControllerConfiguration `json:",inline"` + + ClusterName string `json:"clusterName,omitempty"` +} + +func init() { + SchemeBuilder.Register(&CustomControllerConfiguration{}) +} diff --git a/examples/configfile/custom/v1alpha1/zz_generated.deepcopy.go b/examples/configfile/custom/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..9af38a2730 --- /dev/null +++ b/examples/configfile/custom/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,34 @@ +// +build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomControllerConfiguration) DeepCopyInto(out *CustomControllerConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ControllerConfiguration.DeepCopyInto(&out.ControllerConfiguration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomControllerConfiguration. +func (in *CustomControllerConfiguration) DeepCopy() *CustomControllerConfiguration { + if in == nil { + return nil + } + out := new(CustomControllerConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CustomControllerConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/hack/tools/tools.go b/hack/tools/tools.go index 6c0de6f07d..557bd11c83 100644 --- a/hack/tools/tools.go +++ b/hack/tools/tools.go @@ -22,4 +22,5 @@ package tools import ( _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "github.com/joelanford/go-apidiff" + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" ) diff --git a/hack/verify.sh b/hack/verify.sh index af02aad73f..ffb9974a0a 100755 --- a/hack/verify.sh +++ b/hack/verify.sh @@ -26,3 +26,6 @@ make lint header_text "verifying modules" make modules verify-modules + +header_text "running generate" +make generate diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000000..442966aa82 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,120 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + ioutil "io/ioutil" + "sync" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" +) + +// ControllerConfiguration defines the functions necessary to parse a config file +// and to configure the Options struct for the ctrl.Manager +type ControllerConfiguration interface { + runtime.Object + + // GetControllerConfiguration returns the versioned configuration + GetControllerConfiguration() (v1alpha1.ControllerConfiguration, error) +} + +// DeferredFileLoader is used to setup the file loader for manager.Options +// and a ComponentConfig type +type DeferredFileLoader interface { + GetControllerConfiguration() (v1alpha1.ControllerConfiguration, error) + AtPath(string) DeferredFileLoader + OfKind(ControllerConfiguration) DeferredFileLoader + WithScheme(*runtime.Scheme) DeferredFileLoader +} + +// FromFile will set up the deferred file loader for the configuration +// this will also configure the defaults for the loader if nothing is +// +// Defaults: +// Path: "./config.yaml" +// Kind: GenericControllerConfiguration +func FromFile() DeferredFileLoader { + scheme := runtime.NewScheme() + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + return &deferredFileLoader{ + path: "./config.yaml", + kind: &v1alpha1.GenericControllerConfiguration{}, + scheme: scheme, + } +} + +// deferredFileLoader is used to configure the decoder for loading controller +// runtime component config types +type deferredFileLoader struct { + kind ControllerConfiguration + path string + scheme *runtime.Scheme + once sync.Once + err error +} + +// GetControllerConfiguration will use sync.Once to set the scheme +func (d *deferredFileLoader) GetControllerConfiguration() (v1alpha1.ControllerConfiguration, error) { + d.once.Do(d.loadFile) + return d.kind.GetControllerConfiguration() +} + +// AtPath will set the path to load the file for the decoder +func (d *deferredFileLoader) AtPath(path string) DeferredFileLoader { + d.path = path + return d +} + +// OfKind will set the type to be used for decoding the file into +func (d *deferredFileLoader) OfKind(obj ControllerConfiguration) DeferredFileLoader { + d.kind = obj + return d +} + +// WithScheme will configure the scheme to be used for decoding the file +func (d *deferredFileLoader) WithScheme(scheme *runtime.Scheme) DeferredFileLoader { + d.scheme = scheme + return d +} + +// loadFile is used from the mutex.Once to load the file +func (d *deferredFileLoader) loadFile() { + if d.scheme == nil { + d.err = fmt.Errorf("scheme not supplied to controller configuration loader") + return + } + + content, err := ioutil.ReadFile(d.path) + if err != nil { + d.err = fmt.Errorf("file could not be read from filesystem") + return + } + + codecs := serializer.NewCodecFactory(d.scheme) + + // Regardless of if the bytes are of any external version, + // it will be read successfully and converted into the internal version + if err = runtime.DecodeInto(codecs.UniversalDecoder(), content, d.kind); err != nil { + d.err = fmt.Errorf("could not decode file into runtime.Object") + } + + return +} diff --git a/pkg/config/config_suite_test.go b/pkg/config/config_suite_test.go new file mode 100644 index 0000000000..9a494dafbc --- /dev/null +++ b/pkg/config/config_suite_test.go @@ -0,0 +1,32 @@ +/* +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 config_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" +) + +func TestScheme(t *testing.T) { + RegisterFailHandler(Fail) + suiteName := "Config Suite" + RunSpecsWithDefaultAndCustomReporters(t, suiteName, []Reporter{printer.NewlineReporter{}, printer.NewProwReporter(suiteName)}) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000000..ebd6af4ca9 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,48 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" +) + +var _ = Describe("config", func() { + Describe("FromFile", func() { + + It("should error loading from non existent file", func() { + loader := config.FromFile() + _, err := loader.GetControllerConfiguration() + Expect(err).To(BeNil()) + }) + + It("should load a config from file", func() { + conf := v1alpha1.GenericControllerConfiguration{} + loader := config.FromFile().AtPath("./testdata/config.yaml").OfKind(&conf) + Expect(conf.Namespace).To(Equal("")) + + _, err := loader.GetControllerConfiguration() + Expect(err).To(BeNil()) + + Expect(*conf.LeaderElection.LeaderElect).To(Equal(true)) + Expect(conf.Namespace).To(Equal("default")) + Expect(conf.MetricsBindAddress).To(Equal(":8081")) + }) + }) +}) diff --git a/pkg/config/doc.go b/pkg/config/doc.go new file mode 100644 index 0000000000..51e709d4b0 --- /dev/null +++ b/pkg/config/doc.go @@ -0,0 +1,25 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package config contains functionality for interacting with ComponentConfig +// files +// +// DeferredFileLoader +// +// This uses a deferred file decoding allowing you to chain your configuration +// setup. You can pass this into manager.Options#FromFile and it will load your +// config. +package config diff --git a/pkg/config/example_test.go b/pkg/config/example_test.go new file mode 100644 index 0000000000..751b8a7a8d --- /dev/null +++ b/pkg/config/example_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config_test + +import ( + "fmt" + "os" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/config" + + "sigs.k8s.io/controller-runtime/examples/configfile/custom/v1alpha1" +) + +var scheme = runtime.NewScheme() + +func init() { + _ = v1alpha1.AddToScheme(scheme) +} + +// This example will load a file using GetControllerConfiguration with only +// defaults set. +func ExampleFromFile() { + // This will load a config file from ./config.yaml + loader := config.FromFile() + _, err := loader.GetControllerConfiguration() + if err != nil { + fmt.Println("failed to load config") + os.Exit(1) + } +} + +// This example will load the file from a custom path +func ExampleDeferredFileLoader_atPath() { + loader := config.FromFile() + loader.AtPath("/var/run/controller-runtime/config.yaml") + _, err := loader.GetControllerConfiguration() + if err != nil { + fmt.Println("failed to load config") + os.Exit(1) + } +} + +// This example sets up loader with a custom scheme +func ExampleDeferredFileLoader_withScheme() { + loader := config.FromFile() + loader.WithScheme(scheme) + _, err := loader.GetControllerConfiguration() + if err != nil { + fmt.Println("failed to load config") + os.Exit(1) + } +} + +// This example sets up the loader with a custom scheme and custom type +func ExampleDeferredFileLoader_ofKind() { + loader := config.FromFile() + loader.WithScheme(scheme) + loader.OfKind(&v1alpha1.CustomControllerConfiguration{}) + _, err := loader.GetControllerConfiguration() + if err != nil { + fmt.Println("failed to load config") + os.Exit(1) + } +} diff --git a/pkg/config/testdata/config.yaml b/pkg/config/testdata/config.yaml new file mode 100644 index 0000000000..3010372a04 --- /dev/null +++ b/pkg/config/testdata/config.yaml @@ -0,0 +1,6 @@ +apiServer: controller-runtime.sigs.k8s.io/v1alpha1 +kind: GenericControllerConfiguration +namespace: default +metricsBindAddress: :8081 +leaderElection: + leaderElect: true diff --git a/pkg/config/v1alpha1/doc.go b/pkg/config/v1alpha1/doc.go new file mode 100644 index 0000000000..57a7cff93d --- /dev/null +++ b/pkg/config/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 provides the GenericControllerConfiguration used for +// configuring ctrl.Manager +// +kubebuilder:object:generate=true +package v1alpha1 diff --git a/pkg/config/v1alpha1/register.go b/pkg/config/v1alpha1/register.go new file mode 100644 index 0000000000..3542270dc1 --- /dev/null +++ b/pkg/config/v1alpha1/register.go @@ -0,0 +1,37 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "controller-runtime.sigs.k8s.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register(&GenericControllerConfiguration{}) +} diff --git a/pkg/config/v1alpha1/types.go b/pkg/config/v1alpha1/types.go new file mode 100644 index 0000000000..595d591a26 --- /dev/null +++ b/pkg/config/v1alpha1/types.go @@ -0,0 +1,88 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configv1alpha1 "k8s.io/component-base/config/v1alpha1" +) + +// ControllerConfiguration defines the desired state of GenericControllerConfiguration +type ControllerConfiguration struct { + // SyncPeriod returns the SyncPeriod + // +optional + SyncPeriod *metav1.Duration `json:"syncPeriod,omitempty"` + + // LeaderElection returns the LeaderElection config + // +optional + LeaderElection configv1alpha1.LeaderElectionConfiguration `json:"leaderElection,omitempty"` + + // Namespace returns the namespace for the controller + // +optional + Namespace string `json:"namespace,omitempty"` + + // MetricsBindAddress returns the bind address for the metrics server + // +optional + MetricsBindAddress string `json:"metricsBindAddress,omitempty"` + + // Health contains the controller health information + // +optional + Health ControllerHealth `json:"health,omitempty"` + + // Port returns the Port for the server + // +optional + Port *int `json:"port,omitempty"` + + // Host returns the Host for the server + // +optional + Host string `json:"host,omitempty"` + + // CertDir returns the CertDir + // +optional + CertDir string `json:"certDir,omitempty"` +} + +// ControllerHealth defines the health configs +type ControllerHealth struct { + // HealthProbeBindAddress returns the bind address for the health probe + // +optional + HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"` + + // ReadinessEndpointName returns the readiness endpoint name + // +optional + ReadinessEndpointName string `json:"readinessEndpointName,omitempty"` + + // LivenessEndpointName returns the liveness endpoint name + // +optional + LivenessEndpointName string `json:"livenessEndpointName,omitempty"` +} + +// +kubebuilder:object:root=true + +// GenericControllerConfiguration is the Schema for the GenericControllerConfigurations API +type GenericControllerConfiguration struct { + metav1.TypeMeta `json:",inline"` + + // ControllerConfiguration returns the contfigurations for controllers + ControllerConfiguration `json:",inline"` +} + +// GetControllerConfiguration returns the configuration for controller-runtime +func (c *ControllerConfiguration) GetControllerConfiguration() (ControllerConfiguration, error) { + return *c, nil +} diff --git a/pkg/config/v1alpha1/zz_generated.deepcopy.go b/pkg/config/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..39bcc4a0aa --- /dev/null +++ b/pkg/config/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,77 @@ +// +build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerConfiguration) DeepCopyInto(out *ControllerConfiguration) { + *out = *in + if in.SyncPeriod != nil { + in, out := &in.SyncPeriod, &out.SyncPeriod + *out = new(v1.Duration) + **out = **in + } + in.LeaderElection.DeepCopyInto(&out.LeaderElection) + out.Health = in.Health + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerConfiguration. +func (in *ControllerConfiguration) DeepCopy() *ControllerConfiguration { + if in == nil { + return nil + } + out := new(ControllerConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerHealth) DeepCopyInto(out *ControllerHealth) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerHealth. +func (in *ControllerHealth) DeepCopy() *ControllerHealth { + if in == nil { + return nil + } + out := new(ControllerHealth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericControllerConfiguration) DeepCopyInto(out *GenericControllerConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ControllerConfiguration.DeepCopyInto(&out.ControllerConfiguration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericControllerConfiguration. +func (in *GenericControllerConfiguration) DeepCopy() *GenericControllerConfiguration { + if in == nil { + return nil + } + out := new(GenericControllerConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GenericControllerConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/manager/example_test.go b/pkg/manager/example_test.go index 3d54ca3144..9abc87aae5 100644 --- a/pkg/manager/example_test.go +++ b/pkg/manager/example_test.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client/config" + conf "sigs.k8s.io/controller-runtime/pkg/config" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/manager/signals" @@ -86,3 +87,44 @@ func ExampleManager_start() { os.Exit(1) } } + +// This example will populate Options from a custom config file +// using defaults +func ExampleOptions_andFrom() { + opts := manager.Options{} + _, err := opts.AndFrom(conf.FromFile()) + if err != nil { + log.Error(err, "unable to load config") + os.Exit(1) + } + + cfg, err := config.GetConfig() + if err != nil { + log.Error(err, "unable to get kubeconfig") + os.Exit(1) + } + + mgr, err := manager.New(cfg, opts) + if err != nil { + log.Error(err, "unable to set up manager") + os.Exit(1) + } + log.Info("created manager", "manager", mgr) +} + +// This example will populate Options from a custom config file +// using defaults and will panic if there are errors +func ExampleOptions_andFromOrDie() { + cfg, err := config.GetConfig() + if err != nil { + log.Error(err, "unable to get kubeconfig") + os.Exit(1) + } + + mgr, err := manager.New(cfg, manager.Options{}.AndFromOrDie(conf.FromFile())) + if err != nil { + log.Error(err, "unable to set up manager") + os.Exit(1) + } + log.Info("created manager", "manager", mgr) +} diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index 966f1d701d..8e38afa65a 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -21,10 +21,12 @@ import ( "fmt" "net" "net/http" + "reflect" "time" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -33,6 +35,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/healthz" intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder" "sigs.k8s.io/controller-runtime/pkg/leaderelection" @@ -400,6 +404,100 @@ func New(config *rest.Config, options Options) (Manager, error) { }, nil } +// AndFrom will use a supplied type and convert to Options +// any options already set on Options will be ignored, this is used to allow +// cli flags to override anything specified in the config file +func (o Options) AndFrom(loader config.DeferredFileLoader) (Options, error) { + if o.Scheme != nil { + loader.WithScheme(o.Scheme) + } + newObj, err := loader.GetControllerConfiguration() + if err != nil { + return o, err + } + + o = o.setLeaderElectionConfig(newObj) + + if o.SyncPeriod == nil && newObj.SyncPeriod != nil { + o.SyncPeriod = &newObj.SyncPeriod.Duration + } + + if o.Namespace == "" && newObj.Namespace != "" { + o.Namespace = newObj.Namespace + } + + if o.MetricsBindAddress == "" && newObj.MetricsBindAddress != "" { + o.MetricsBindAddress = newObj.MetricsBindAddress + } + + if o.HealthProbeBindAddress == "" && newObj.Health.HealthProbeBindAddress != "" { + o.HealthProbeBindAddress = newObj.Health.HealthProbeBindAddress + } + + if o.ReadinessEndpointName == "" && newObj.Health.ReadinessEndpointName != "" { + o.ReadinessEndpointName = newObj.Health.ReadinessEndpointName + } + + if o.LivenessEndpointName == "" && newObj.Health.LivenessEndpointName != "" { + o.LivenessEndpointName = newObj.Health.LivenessEndpointName + } + + if o.Port == 0 && newObj.Port != nil { + o.Port = *newObj.Port + } + + if o.Host == "" && newObj.Host != "" { + o.Host = newObj.Host + } + + if o.CertDir == "" && newObj.CertDir != "" { + o.CertDir = newObj.CertDir + } + + return o, nil +} + +// AndFromOrDie will use options.AndFrom() and will panic if there are errors +func (o Options) AndFromOrDie(loader config.DeferredFileLoader) Options { + o, err := o.AndFrom(loader) + if err != nil { + panic("could not parse config file") + } + return o +} + +func (o Options) setLeaderElectionConfig(obj v1alpha1.ControllerConfiguration) Options { + if o.LeaderElection == false && obj.LeaderElection.LeaderElect != nil { + o.LeaderElection = *obj.LeaderElection.LeaderElect + } + + if o.LeaderElectionResourceLock == "" && obj.LeaderElection.ResourceLock != "" { + o.LeaderElectionResourceLock = obj.LeaderElection.ResourceLock + } + + if o.LeaderElectionNamespace == "" && obj.LeaderElection.ResourceNamespace != "" { + o.LeaderElectionNamespace = obj.LeaderElection.ResourceNamespace + } + + if o.LeaderElectionID == "" && obj.LeaderElection.ResourceName != "" { + o.LeaderElectionID = obj.LeaderElection.ResourceName + } + + if o.LeaseDuration == nil && !reflect.DeepEqual(obj.LeaderElection.LeaseDuration, metav1.Duration{}) { + o.LeaseDuration = &obj.LeaderElection.LeaseDuration.Duration + } + + if o.RenewDeadline == nil && !reflect.DeepEqual(obj.LeaderElection.RenewDeadline, metav1.Duration{}) { + o.RenewDeadline = &obj.LeaderElection.RenewDeadline.Duration + } + + if o.RetryPeriod == nil && !reflect.DeepEqual(obj.LeaderElection.RetryPeriod, metav1.Duration{}) { + o.RetryPeriod = &obj.LeaderElection.RetryPeriod.Duration + } + + return o +} + // DefaultNewClient creates the default caching client func DefaultNewClient(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) { // Create the Client for Write operations. diff --git a/pkg/manager/manager_test.go b/pkg/manager/manager_test.go index 38c831e8ec..6b72d5a59e 100644 --- a/pkg/manager/manager_test.go +++ b/pkg/manager/manager_test.go @@ -35,9 +35,13 @@ import ( "go.uber.org/goleak" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" "k8s.io/client-go/tools/leaderelection/resourcelock" + configv1alpha1 "k8s.io/component-base/config/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/cache/informertest" @@ -122,6 +126,130 @@ var _ = Describe("manger.Manager", func() { close(done) }) + It("should be able to load Options from cfg.ControllerConfiguration type", func(done Done) { + duration := metav1.Duration{Duration: 48 * time.Hour} + port := int(6090) + leaderElect := false + + ccfg := &v1alpha1.GenericControllerConfiguration{ + ControllerConfiguration: v1alpha1.ControllerConfiguration{ + SyncPeriod: &duration, + LeaderElection: configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: &leaderElect, + ResourceLock: "leases", + ResourceNamespace: "default", + ResourceName: "ctrl-lease", + LeaseDuration: duration, + RenewDeadline: duration, + RetryPeriod: duration, + }, + Namespace: "default", + MetricsBindAddress: ":6000", + Health: v1alpha1.ControllerHealth{ + HealthProbeBindAddress: "6060", + ReadinessEndpointName: "/readyz", + LivenessEndpointName: "/livez", + }, + Port: &port, + Host: "localhost", + CertDir: "/certs", + }, + } + + m, err := Options{}.AndFrom(&fakeDeferredLoader{obj: ccfg}) + Expect(err).To(BeNil()) + + Expect(*m.SyncPeriod).To(Equal(duration.Duration)) + Expect(m.LeaderElection).To(Equal(leaderElect)) + Expect(m.LeaderElectionResourceLock).To(Equal("leases")) + Expect(m.LeaderElectionNamespace).To(Equal("default")) + Expect(m.LeaderElectionID).To(Equal("ctrl-lease")) + Expect(m.LeaseDuration.String()).To(Equal(duration.Duration.String())) + Expect(m.RenewDeadline.String()).To(Equal(duration.Duration.String())) + Expect(m.RetryPeriod.String()).To(Equal(duration.Duration.String())) + Expect(m.Namespace).To(Equal("default")) + Expect(m.MetricsBindAddress).To(Equal(":6000")) + Expect(m.HealthProbeBindAddress).To(Equal("6060")) + Expect(m.ReadinessEndpointName).To(Equal("/readyz")) + Expect(m.LivenessEndpointName).To(Equal("/livez")) + Expect(m.Port).To(Equal(port)) + Expect(m.Host).To(Equal("localhost")) + Expect(m.CertDir).To(Equal("/certs")) + + close(done) + }) + + It("should be able to keep Options when cfg.ControllerConfiguration set", func(done Done) { + optDuration := time.Duration(2) + duration := metav1.Duration{Duration: 48 * time.Hour} + port := int(6090) + leaderElect := false + + ccfg := &v1alpha1.GenericControllerConfiguration{ + ControllerConfiguration: v1alpha1.ControllerConfiguration{ + SyncPeriod: &duration, + LeaderElection: configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: &leaderElect, + ResourceLock: "leases", + ResourceNamespace: "default", + ResourceName: "ctrl-lease", + LeaseDuration: duration, + RenewDeadline: duration, + RetryPeriod: duration, + }, + Namespace: "default", + MetricsBindAddress: ":6000", + Health: v1alpha1.ControllerHealth{ + HealthProbeBindAddress: "6060", + ReadinessEndpointName: "/readyz", + LivenessEndpointName: "/livez", + }, + Port: &port, + Host: "localhost", + CertDir: "/certs", + }, + } + + m, err := Options{ + SyncPeriod: &optDuration, + LeaderElection: true, + LeaderElectionResourceLock: "configmaps", + LeaderElectionNamespace: "ctrl", + LeaderElectionID: "ctrl-configmap", + LeaseDuration: &optDuration, + RenewDeadline: &optDuration, + RetryPeriod: &optDuration, + Namespace: "ctrl", + MetricsBindAddress: ":7000", + HealthProbeBindAddress: "5000", + ReadinessEndpointName: "/readiness", + LivenessEndpointName: "/liveness", + Port: 8080, + Host: "example.com", + CertDir: "/pki", + }.AndFrom(&fakeDeferredLoader{obj: ccfg}) + Expect(err).To(BeNil()) + + Expect(m.SyncPeriod.String()).To(Equal(optDuration.String())) + Expect(m.LeaderElection).To(Equal(true)) + Expect(m.LeaderElectionResourceLock).To(Equal("configmaps")) + Expect(m.LeaderElectionNamespace).To(Equal("ctrl")) + Expect(m.LeaderElectionID).To(Equal("ctrl-configmap")) + Expect(m.LeaseDuration.String()).To(Equal(optDuration.String())) + Expect(m.RenewDeadline.String()).To(Equal(optDuration.String())) + Expect(m.RetryPeriod.String()).To(Equal(optDuration.String())) + Expect(m.Namespace).To(Equal("ctrl")) + Expect(m.MetricsBindAddress).To(Equal(":7000")) + Expect(m.HealthProbeBindAddress).To(Equal("5000")) + Expect(m.ReadinessEndpointName).To(Equal("/readiness")) + Expect(m.LivenessEndpointName).To(Equal("/liveness")) + Expect(m.Port).To(Equal(8080)) + Expect(m.Host).To(Equal("example.com")) + Expect(m.CertDir).To(Equal("/pki")) + + close(done) + }) + It("should lazily initialize a webhook server if needed", func(done Done) { By("creating a manager with options") m, err := New(cfg, Options{Port: 9440, Host: "foo.com"}) @@ -1470,3 +1598,21 @@ type runnableError struct { func (runnableError) Error() string { return "not feeling like that" } + +type fakeDeferredLoader struct { + obj *v1alpha1.GenericControllerConfiguration +} + +func (f *fakeDeferredLoader) GetControllerConfiguration() (v1alpha1.ControllerConfiguration, error) { + return f.obj.ControllerConfiguration, nil +} + +func (f *fakeDeferredLoader) AtPath(path string) config.DeferredFileLoader { + return f +} +func (f *fakeDeferredLoader) OfKind(cfg config.ControllerConfiguration) config.DeferredFileLoader { + return f +} +func (f *fakeDeferredLoader) WithScheme(scheme *runtime.Scheme) config.DeferredFileLoader { + return f +}