|
| 1 | +# ComponentConfig Controller Runtime Support |
| 2 | + |
| 3 | +Author: @christopherhein |
| 4 | +Last Updated on: 02/24/2020 |
| 5 | + |
| 6 | +## Table of Contents |
| 7 | + |
| 8 | +<!--ts--> |
| 9 | + * [ComponentConfig Controller Runtime Support](#componentconfig-controller-runtime-support) |
| 10 | + * [Table of Contents](#table-of-contents) |
| 11 | + * [Summary](#summary) |
| 12 | + * [Motivation](#motivation) |
| 13 | + * [Links to Open Issues](#links-to-open-issues) |
| 14 | + * [Goals](#goals) |
| 15 | + * [Non-Goals/Future Work](#non-goalsfuture-work) |
| 16 | + * [Proposal](#proposal) |
| 17 | + * [ComponentConfig Load Order](#componentconfig-load-order) |
| 18 | + * [User Stories](#user-stories) |
| 19 | + * [Controller Author with controller-runtime](#controller-author-with-controller-runtime) |
| 20 | + * [Controller Author with kubebuilder (tbd proposal for kubebuilder)](#controller-author-with-kubebuilder-tbd-proposal-for-kubebuilder) |
| 21 | + * [Controller User without modifications to config](#controller-user-without-modifications-to-config) |
| 22 | + * [Controller User with modifications to config](#controller-user-with-modifications-to-config) |
| 23 | + * [Risks and Mitigations](#risks-and-mitigations) |
| 24 | + * [Alternatives](#alternatives) |
| 25 | + * [Implementation History](#implementation-history) |
| 26 | + |
| 27 | +<!--te--> |
| 28 | + |
| 29 | +## Summary |
| 30 | + |
| 31 | +Currently all any controller that uses `controller-runtime` needs to configure the `ctrl.Manager` by using flags or hard coding values into the initialization methods. Core Kubernetes resources have started to move away from using flags as a mechanism for configuring the component and standardized along the pattern of [`ComponentConfig`](https://github.com/kubernetes/enhancements/blob/master/keps/sig-cluster-lifecycle/wgs/0032-create-a-k8s-io-component-repo.md#part-1-componentconfig). This proposal is to bring `ComponentConfig` patterns into `controller-runtime` to allow controller authors to make `go` types backed by apimachinery to unmarshal and configure the `ctrl.Manager` reducing the flags and allowing code based tools to easily configure controllers instead of requiring them to mutate CLI args. |
| 32 | + |
| 33 | +## Motivation |
| 34 | + |
| 35 | +This change is important because: |
| 36 | +- it will help make it easier for controllers to be configured by other machine processes |
| 37 | +- it will reduce the upfront flags required to start a controller |
| 38 | +- allow for more configuration types which flags don't natively support |
| 39 | + |
| 40 | +### Links to Open Issues |
| 41 | + |
| 42 | +- [Provide a ComponentConfig to tweak the Manager](https://github.com/kubernetes-sigs/controller-runtime/issues/518) |
| 43 | +- [Reduce command line flag boilerplate](https://github.com/kubernetes-sigs/controller-runtime/issues/207) |
| 44 | +- [Implement ComponentConfig by default & stop using (most) flags](https://github.com/kubernetes-sigs/kubebuilder/issues/722) |
| 45 | + |
| 46 | +### Goals |
| 47 | + |
| 48 | +- Provide an interface for pulling configuration data out of exposed `ComponentConfig` types (see below for implementation) |
| 49 | +- Provide a new `ctrl.NewFromComponentConfig()` function for initializing a manager |
| 50 | + |
| 51 | +### Non-Goals/Future Work |
| 52 | + |
| 53 | +- `kubebuilder` implementation and design in another PR |
| 54 | +- Changing the default `controller-runtime` implementation |
| 55 | +- Dynamically reloading ComponentConfig object |
| 56 | + |
| 57 | +## Proposal |
| 58 | + |
| 59 | +The `ctrl.Manager` _SHOULD_ to be able to support load configurations from an interface with getters to pull the specific configuration parameters out of it so that we can use `ComponentConfig` like objects to load the configuration from. |
| 60 | + |
| 61 | +Without breaking the current `ctrl.NewManager` which uses an exported `ctrl.Options{}` the `manager.go` can expose a new func, `NewFromComponentConfig()` this would be able to loop through the getters to hydrate an internal `ctrl.Options{}` and pass that into `New()`. |
| 62 | + |
| 63 | +File: _`pkg/manager/manager.go`_ |
| 64 | +```golang |
| 65 | +// ManagerConfiguration defines what the ComponentConfig object for ControllerRuntime needs to support |
| 66 | +type ManagerConfiguration interface { |
| 67 | + GetSyncPeriod() *time.Duration |
| 68 | + |
| 69 | + GetLeaderElection() bool |
| 70 | + GetLeaderElectionNamespace() string |
| 71 | + GetLeaderElectionID() string |
| 72 | + |
| 73 | + GetLeaseDuration() *time.Duration |
| 74 | + GetRenewDeadline() *time.Duration |
| 75 | + GetRetryPeriod() *time.Duration |
| 76 | + |
| 77 | + GetNamespace() string |
| 78 | + GetMetricsBindAddress() string |
| 79 | + GetHealthProbeBindAddress() string |
| 80 | + |
| 81 | + GetReadinessEndpointName() string |
| 82 | + GetLivenessEndpointName() string |
| 83 | + |
| 84 | + GetPort() int |
| 85 | + GetHost() string |
| 86 | + |
| 87 | + GetCertDir() string |
| 88 | +} |
| 89 | + |
| 90 | +func NewFromComponentConfig(config *rest.Config, scheme *runtime.Scheme, managerconfig ManagerConfiguration) (Manager, error) { |
| 91 | + options := Options{} |
| 92 | + |
| 93 | + if scheme != nil { |
| 94 | + options.Scheme = scheme |
| 95 | + } |
| 96 | + |
| 97 | + // Loop through getters |
| 98 | + if managerconfig.GetLeaderElection() { |
| 99 | + managerconfig.GetLeaderElection() |
| 100 | + } |
| 101 | + // ... |
| 102 | + |
| 103 | + return New(config, options) |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +#### ComponentConfig Load Order |
| 108 | + |
| 109 | + |
| 110 | + |
| 111 | +Within a separate design (link once created) this will require controller authors to generate a type that implements the `ManagerConfiguration` interface. The following is a sample of what this looks like: |
| 112 | + |
| 113 | +```golang |
| 114 | +package config |
| 115 | + |
| 116 | +import ( |
| 117 | + "time" |
| 118 | + |
| 119 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 120 | +) |
| 121 | + |
| 122 | +type ControllerNameConfigurationSpec struct { |
| 123 | + LeaderElection ControllerNameConfigurationLeaderElection `json:"leaderElection,omitempty"` |
| 124 | + |
| 125 | + LeaseDuration *time.Duration `json:"leaseDuration,omitempty"` |
| 126 | + RenewDeadline *time.Duration `json:"renewDeadline,omitempty"` |
| 127 | + RetryPeriod *time.Duration`json:"retryPeriod,omitempty"` |
| 128 | + |
| 129 | + Namespace string `json:"namespace,omitempty"` |
| 130 | + |
| 131 | + MetricsBindAddress string `json:"metricsBindAddress,omitempty"` |
| 132 | + |
| 133 | + Health ControllerNameConfigurationHealth |
| 134 | + |
| 135 | + Port int `json:"port,omitempty"` |
| 136 | + Host string `json:"host,omitempty"` |
| 137 | + |
| 138 | + CertDir string `json:"certDir,omitempty"` |
| 139 | +} |
| 140 | + |
| 141 | +type ControllerNameConfigurationLeaderElection struct { |
| 142 | + Enabled bool `json:"enabled,omitempty"` |
| 143 | + Namespace string `json:"namespace,omitempty"` |
| 144 | + ID string `json:"id,omitempty"` |
| 145 | +} |
| 146 | + |
| 147 | +type ControllerNameConfigurationHealth struct { |
| 148 | + HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"` |
| 149 | + |
| 150 | + ReadinessEndpointName string `json:"readinessEndpointName,omitempty"` |
| 151 | + LivenessEndpointName string `json:"livenessEndpointName,omitempty"` |
| 152 | +} |
| 153 | + |
| 154 | +type ControllerNameConfiguration struct { |
| 155 | + metav1.TypeMeta |
| 156 | + |
| 157 | + Spec ControllerNameConfigurationSpec `json:"spec"` |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +With the above `struct` we would need to implement the getters to satisfy the `ManagerConfiguration` interface, these could be automatically generated when you `init` or `create componentconfig` from the `kubebuilder` CLI. This could be something as simple as the following. |
| 162 | + |
| 163 | +```golang |
| 164 | +package config |
| 165 | + |
| 166 | +import ( |
| 167 | + "time" |
| 168 | +) |
| 169 | + |
| 170 | +func (in *ControllerNameConfiguration) GetSyncPeriod() *time.Duration {} |
| 171 | +func (in *ControllerNameConfiguration) GetLeaderElection() bool {} |
| 172 | +func (in *ControllerNameConfiguration) GetLeaderElectionNamespace() string {} |
| 173 | +func (in *ControllerNameConfiguration) GetLeaderElectionID() string {} |
| 174 | + |
| 175 | +func (in *ControllerNameConfiguration) GetLeaseDuration() *time.Duration {} |
| 176 | +func (in *ControllerNameConfiguration) GetRenewDeadline() *time.Duration {} |
| 177 | +func (in *ControllerNameConfiguration) GetRetryPeriod() *time.Duration {} |
| 178 | + |
| 179 | +func (in *ControllerNameConfiguration) GetNamespace() string {} |
| 180 | +func (in *ControllerNameConfiguration) GetMetricsBindAddress() string {} |
| 181 | +func (in *ControllerNameConfiguration) GetHealthProbeBindAddress() string {} |
| 182 | + |
| 183 | +func (in *ControllerNameConfiguration) GetReadinessEndpointName() string {} |
| 184 | +func (in *ControllerNameConfiguration) GetLivenessEndpointName() string {} |
| 185 | + |
| 186 | +func (in *ControllerNameConfiguration) GetPort() int {} |
| 187 | +func (in *ControllerNameConfiguration) GetHost() string {} |
| 188 | + |
| 189 | +func (in *ControllerNameConfiguration) GetCertDir() string {} |
| 190 | +``` |
| 191 | + |
| 192 | +Besides the implementation of the `ComponentConfig` The controller author as it stands would also need to implement the unmarshalling of the `ConfigMap` into the `ComponentConfig`, for this `controller-runtime` could expose helper methods to load a file from disk, unmarshal to the struct and pass the pointer into the `NewFromComponentConfig()` to return the `ctrl.Manager` |
| 193 | + |
| 194 | +### User Stories |
| 195 | + |
| 196 | +#### Controller Author with `controller-runtime` |
| 197 | + |
| 198 | +- Implement `ComponentConfig` type |
| 199 | +- Implement `ManagerConfiguration` interface for `ComponentConfig` object |
| 200 | +- Set up `ConfigMap` unmarshalling into `ComponentConfig` type |
| 201 | +- Initialize `ctrl.Manager` with `NewFromComponentConfig` |
| 202 | +- Build custom controller as usual |
| 203 | + |
| 204 | +#### Controller Author with `kubebuilder` (tbd proposal for `kubebuilder`) |
| 205 | + |
| 206 | +- Initialize `kubebuilder` project using `--component-config-name=XYZConfiguration` |
| 207 | +- Build custom controller as usual |
| 208 | + |
| 209 | +#### Controller User without modifications to config |
| 210 | + |
| 211 | +_Provided that the controller provides manifests_ |
| 212 | + |
| 213 | +- Apply the controller to the cluster |
| 214 | +- Deploy custom resources |
| 215 | + |
| 216 | +#### Controller User with modifications to config |
| 217 | + |
| 218 | +- _Following from previous example without changes_ |
| 219 | +- Create a new `ConfigMap` for changes |
| 220 | +- Modify the `controller-runtime` pod to use the new `ConfigMap` |
| 221 | +- Apply the controller to the cluster |
| 222 | +- Deploy custom resources |
| 223 | + |
| 224 | + |
| 225 | +### Risks and Mitigations |
| 226 | + |
| 227 | +- Given that this isn't changing the core Manager initialization for `controller-runtime` it's fairly low risk |
| 228 | +- If the underlaying `ctrl.Options{}` |
| 229 | + |
| 230 | +## Alternatives |
| 231 | + |
| 232 | +* `NewFromComponentConfig()` could load the object from disk and hydrate the `ComponentConfig` type |
| 233 | + |
| 234 | +## Implementation History |
| 235 | + |
| 236 | +- [x] 02/19/2020: Proposed idea in an issue or [community meeting] |
| 237 | +- [x] 02/24/2020: Proposal submitted to `controller-runtime` |
| 238 | + |
| 239 | + |
| 240 | +<!-- Links --> |
| 241 | +[community meeting]: https://docs.google.com/document/d/1Ih-2cgg1bUrLwLVTB9tADlPcVdgnuMNBGbUl4D-0TIk |
0 commit comments