|
| 1 | +# ComponentConfig Controller Runtime Support |
| 2 | +Author: @christopherhein |
| 3 | + |
| 4 | +Last Updated on: 03/02/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 | + * [Default ComponentConfig Type](#default-componentconfig-type) |
| 19 | + * [Caveats](#caveats) |
| 20 | + * [Kubebuilder Scaffolding Example](#kubebuilder-scaffolding-example) |
| 21 | + * [User Stories](#user-stories) |
| 22 | + * [Controller Author with controller-runtime](#controller-author-with-controller-runtime) |
| 23 | + * [Controller Author with kubebuilder (tbd proposal for kubebuilder)](#controller-author-with-kubebuilder-tbd-proposal-for-kubebuilder) |
| 24 | + * [Controller User without modifications to config](#controller-user-without-modifications-to-config) |
| 25 | + * [Controller User with modifications to config](#controller-user-with-modifications-to-config) |
| 26 | + * [Risks and Mitigations](#risks-and-mitigations) |
| 27 | + * [Alternatives](#alternatives) |
| 28 | + * [Implementation History](#implementation-history) |
| 29 | + |
| 30 | +<!--te--> |
| 31 | + |
| 32 | +## Summary |
| 33 | + |
| 34 | +Currently controllers that use `controller-runtime` need to configure the `ctrl.Manager` by using flags or hardcoding values into the initialization methods. Core Kubernetes has started to move away from using flags as a mechanism for configuring components and standardized along the pattern of [`ComponentConfig` or Versioned Component Configuration Files](https://docs.google.com/document/d/1FdaEJUEh091qf5B98HM6_8MS764iXrxxigNIdwHYW9c/edit). 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. |
| 35 | + |
| 36 | + |
| 37 | +## Motivation |
| 38 | + |
| 39 | +This change is important because: |
| 40 | +- it will help make it easier for controllers to be configured by other machine processes |
| 41 | +- it will reduce the required flags required to start a controller |
| 42 | +- allow for more configuration types which flags don't natively support |
| 43 | +- allow using and upgrading older configurations avoiding breaking changes in flags |
| 44 | + |
| 45 | +### Links to Open Issues |
| 46 | + |
| 47 | +- [#518 Provide a ComponentConfig to tweak the Manager](https://github.com/kubernetes-sigs/controller-runtime/issues/518) |
| 48 | +- [#207 Reduce command line flag boilerplate](https://github.com/kubernetes-sigs/controller-runtime/issues/207) |
| 49 | +- [#722 Implement ComponentConfig by default & stop using (most) flags](https://github.com/kubernetes-sigs/kubebuilder/issues/722) |
| 50 | + |
| 51 | +### Goals |
| 52 | + |
| 53 | +- Provide an interface for pulling configuration data out of exposed `ComponentConfig` types (see below for implementation) |
| 54 | +- Provide a new `ctrl.NewFromComponentConfig()` function for initializing a manager |
| 55 | +- Provide a `DefaultControllerConfig` to make the switch easier |
| 56 | + |
| 57 | +### Non-Goals/Future Work |
| 58 | + |
| 59 | +- `kubebuilder` implementation and design in another PR |
| 60 | +- Changing the default `controller-runtime` implementation |
| 61 | +- Dynamically reloading ComponentConfig object |
| 62 | +- Providing `flags` interface and overrides |
| 63 | + |
| 64 | +## Proposal |
| 65 | + |
| 66 | +The `ctrl.Manager` _SHOULD_ support loading configurations from `ComponentConfig` like objects. |
| 67 | +An interface for that object with getters for the specific configuration parameters is created to bridge existing patterns. |
| 68 | + |
| 69 | +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()`. |
| 70 | + |
| 71 | +File: _`pkg/manager/manager.go`_ |
| 72 | +```golang |
| 73 | +// ManagerConfiguration defines what the ComponentConfig object for ControllerRuntime needs to support |
| 74 | +type ManagerConfiguration interface { |
| 75 | + GetSyncPeriod() *time.Duration |
| 76 | + |
| 77 | + GetLeaderElection() bool |
| 78 | + GetLeaderElectionNamespace() string |
| 79 | + GetLeaderElectionID() string |
| 80 | + |
| 81 | + GetLeaseDuration() *time.Duration |
| 82 | + GetRenewDeadline() *time.Duration |
| 83 | + GetRetryPeriod() *time.Duration |
| 84 | + |
| 85 | + GetNamespace() string |
| 86 | + GetMetricsBindAddress() string |
| 87 | + GetHealthProbeBindAddress() string |
| 88 | + |
| 89 | + GetReadinessEndpointName() string |
| 90 | + GetLivenessEndpointName() string |
| 91 | + |
| 92 | + GetPort() int |
| 93 | + GetHost() string |
| 94 | + |
| 95 | + GetCertDir() string |
| 96 | +} |
| 97 | + |
| 98 | +func NewFromComponentConfig(config *rest.Config, scheme *runtime.Scheme,filename string, managerconfig ManagerConfiguration) (Manager, error) { |
| 99 | + codecs := serializer.NewCodecFactory(scheme) |
| 100 | + if err := decodeComponentConfigFileInto(codecs, filename, componentconfig); err != nil { |
| 101 | + |
| 102 | + } |
| 103 | + options := Options{} |
| 104 | + |
| 105 | + if scheme != nil { |
| 106 | + options.Scheme = scheme |
| 107 | + } |
| 108 | + |
| 109 | + // Loop through getters |
| 110 | + if managerconfig.GetLeaderElection() { |
| 111 | + managerconfig.GetLeaderElection() |
| 112 | + } |
| 113 | + // ... |
| 114 | + |
| 115 | + return New(config, options) |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +#### ComponentConfig Load Order |
| 120 | + |
| 121 | + |
| 122 | + |
| 123 | +#### Default ComponentConfig Type |
| 124 | + |
| 125 | +To enable `controller-runtime` to have a default `ComponentConfig` struct which can be used instead of requiring each controller or extension to build it's own `ComponentConfig` type, we can create a `DefaultControllerConfiguration` type which can exist in `pkg/api/v1alpha1/types.go`. This will allow the controller authors to use this before needing to implement their own type with additional configs. |
| 126 | + |
| 127 | +```golang |
| 128 | +package v1alpha1 |
| 129 | + |
| 130 | +import ( |
| 131 | + "time" |
| 132 | + |
| 133 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 134 | + configv1alpha1 "sigs.k8s.io/component-base/config/v1alpha1" |
| 135 | +) |
| 136 | + |
| 137 | +// DefaultControllerConfigurationSpec defines the desired state of DefaultControllerConfiguration |
| 138 | +type DefaultControllerConfigurationSpec struct { |
| 139 | + SyncPeriod *time.Duration `json:"syncPeriod,omitempty"` |
| 140 | + |
| 141 | + LeaderElection configv1alpha1.LeaderElectionConfiguration `json:"leaderElection,omitempty"` |
| 142 | + |
| 143 | + MetricsBindAddress string `json:"metricsBindAddress,omitempty"` |
| 144 | + |
| 145 | + Health DefaultControllerConfigurationHealth `json:"health,omitempty"` |
| 146 | + |
| 147 | + Port *int `json:"port,omitempty"` |
| 148 | + Host string `json:"host,omitempty"` |
| 149 | + |
| 150 | + CertDir string `json:"certDir,omitempty"` |
| 151 | +} |
| 152 | + |
| 153 | +// DefaultControllerConfigurationHealth defines the health configs |
| 154 | +type DefaultControllerConfigurationHealth struct { |
| 155 | + HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"` |
| 156 | + |
| 157 | + ReadinessEndpointName string `json:"readinessEndpointName,omitempty"` |
| 158 | + LivenessEndpointName string `json:"livenessEndpointName,omitempty"` |
| 159 | +} |
| 160 | + |
| 161 | +// DefaultControllerConfiguration is the Schema for the DefaultControllerConfigurations API |
| 162 | +type DefaultControllerConfiguration struct { |
| 163 | + metav1.TypeMeta `json:",inline"` |
| 164 | + |
| 165 | + Spec DefaultControllerConfigurationSpec `json:"spec,omitempty"` |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +This would allow a controller author to use this struct with any config that supports the json/yaml structure. For example a controller author could define their `Kind` as `FoobarControllerConfiguration` and have it defined as the following. |
| 170 | + |
| 171 | +```yaml |
| 172 | +# config.yaml |
| 173 | +apiVersion: somedomain.io/v1alpha1 |
| 174 | +kind: FoobarControllerConfiguration |
| 175 | +spec: |
| 176 | + port: 9443 |
| 177 | + metricsBindAddress: ":8080" |
| 178 | + leaderElection: |
| 179 | + leaderElect: false |
| 180 | +``` |
| 181 | +
|
| 182 | +Given the following config and `DefaultControllerConfiguration` we'd be able to initialize the controller using the following. |
| 183 | + |
| 184 | + |
| 185 | +```golang |
| 186 | + mgr, err := ctrl.NewManagerFromComponentConfig(ctrl.GetConfigOrDie(), scheme, configname, &defaultv1alpha1.DefaultControllerConfiguration{}) |
| 187 | + if err != nil { |
| 188 | + // ... |
| 189 | + } |
| 190 | +``` |
| 191 | + |
| 192 | +The above example uses `configname` which is the name of the file to load the configuration from and uses `scheme` to get the specific serializer, eg `serializer.NewCodecFactory(scheme)`. This will allow the configuration to be unmarshalled into the `runtime.Object` type and passed into the |
| 193 | +`ctrl.NewManagerFromComponentConfig()` as a `ManagerConfiguration` interface. |
| 194 | + |
| 195 | +##### Caveats |
| 196 | + |
| 197 | +> ⚠️ Using `DecodeComponentConfigFileInto` does not support the overrides for flags, this is something that is left up to the controller author since they could be using many different flagging interfaces. eg [`flag`](https://golang.org/pkg/flag/), [`pflag`](https://godoc.org/github.com/spf13/pflag), [`flagnum`](https://godoc.org/github.com/luci/luci-go/common/flag/flagenum) and `controller-runtime` should be agnostic to the CLI implementation. |
| 198 | + |
| 199 | +#### Kubebuilder Scaffolding Example |
| 200 | + |
| 201 | +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: |
| 202 | + |
| 203 | +```golang |
| 204 | +package config |
| 205 | +
|
| 206 | +import ( |
| 207 | + "time" |
| 208 | +
|
| 209 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 210 | + configv1alpha1 "sigs.k8s.io/component-base/config/v1alpha1" |
| 211 | +) |
| 212 | +
|
| 213 | +type ControllerNameConfigurationSpec struct { |
| 214 | + SyncPeriod *time.Duration `json:"syncPeriod,omitempty"` |
| 215 | + |
| 216 | + LeaderElection configv1alpha1.LeaderElectionConfiguration `json:"leaderElection,omitempty"` |
| 217 | + |
| 218 | + MetricsBindAddress string `json:"metricsBindAddress,omitempty"` |
| 219 | + |
| 220 | + Health ControllerNameConfigurationHealth |
| 221 | + |
| 222 | + Port int `json:"port,omitempty"` |
| 223 | + Host string `json:"host,omitempty"` |
| 224 | + |
| 225 | + CertDir string `json:"certDir,omitempty"` |
| 226 | +} |
| 227 | + |
| 228 | +type ControllerNameConfigurationHealth struct { |
| 229 | + HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"` |
| 230 | + |
| 231 | + ReadinessEndpointName string `json:"readinessEndpointName,omitempty"` |
| 232 | + LivenessEndpointName string `json:"livenessEndpointName,omitempty"` |
| 233 | +} |
| 234 | + |
| 235 | +type ControllerNameConfiguration struct { |
| 236 | + metav1.TypeMeta |
| 237 | + |
| 238 | + Spec ControllerNameConfigurationSpec `json:"spec"` |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | +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. |
| 243 | + |
| 244 | +```golang |
| 245 | +package config |
| 246 | + |
| 247 | +import ( |
| 248 | + "time" |
| 249 | +) |
| 250 | + |
| 251 | +func (in *ControllerNameConfiguration) GetSyncPeriod() *time.Duration {} |
| 252 | +func (in *ControllerNameConfiguration) GetLeaderElection() bool {} |
| 253 | +func (in *ControllerNameConfiguration) GetLeaderElectionNamespace() string {} |
| 254 | +func (in *ControllerNameConfiguration) GetLeaderElectionID() string {} |
| 255 | + |
| 256 | +func (in *ControllerNameConfiguration) GetLeaseDuration() *time.Duration {} |
| 257 | +func (in *ControllerNameConfiguration) GetRenewDeadline() *time.Duration {} |
| 258 | +func (in *ControllerNameConfiguration) GetRetryPeriod() *time.Duration {} |
| 259 | + |
| 260 | +func (in *ControllerNameConfiguration) GetNamespace() string {} |
| 261 | +func (in *ControllerNameConfiguration) GetMetricsBindAddress() string {} |
| 262 | +func (in *ControllerNameConfiguration) GetHealthProbeBindAddress() string {} |
| 263 | + |
| 264 | +func (in *ControllerNameConfiguration) GetReadinessEndpointName() string {} |
| 265 | +func (in *ControllerNameConfiguration) GetLivenessEndpointName() string {} |
| 266 | + |
| 267 | +func (in *ControllerNameConfiguration) GetPort() int {} |
| 268 | +func (in *ControllerNameConfiguration) GetHost() string {} |
| 269 | + |
| 270 | +func (in *ControllerNameConfiguration) GetCertDir() string {} |
| 271 | +``` |
| 272 | + |
| 273 | +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` |
| 274 | + |
| 275 | +## User Stories |
| 276 | + |
| 277 | +### Controller Author with `controller-runtime` |
| 278 | + |
| 279 | +- Implement `ComponentConfig` type |
| 280 | +- Implement `ManagerConfiguration` interface for `ComponentConfig` object |
| 281 | +- Set up `ConfigMap` unmarshalling into `ComponentConfig` type |
| 282 | +- Initialize `ctrl.Manager` with `NewFromComponentConfig` |
| 283 | +- Build custom controller as usual |
| 284 | + |
| 285 | +### Controller Author with `kubebuilder` (tbd proposal for `kubebuilder`) |
| 286 | + |
| 287 | +- Initialize `kubebuilder` project using `--component-config-name=XYZConfiguration` |
| 288 | +- Build custom controller as usual |
| 289 | + |
| 290 | +### Controller User without modifications to config |
| 291 | + |
| 292 | +_Provided that the controller provides manifests_ |
| 293 | + |
| 294 | +- Apply the controller to the cluster |
| 295 | +- Deploy custom resources |
| 296 | + |
| 297 | +### Controller User with modifications to config |
| 298 | + |
| 299 | +- _Following from previous example without changes_ |
| 300 | +- Create a new `ConfigMap` for changes |
| 301 | +- Modify the `controller-runtime` pod to use the new `ConfigMap` |
| 302 | +- Apply the controller to the cluster |
| 303 | +- Deploy custom resources |
| 304 | + |
| 305 | + |
| 306 | +## Risks and Mitigations |
| 307 | + |
| 308 | +- Given that this isn't changing the core Manager initialization for `controller-runtime` it's fairly low risk |
| 309 | + |
| 310 | +## Alternatives |
| 311 | + |
| 312 | +* `NewFromComponentConfig()` could load the object from disk based on the file name and hydrate the `ComponentConfig` type. |
| 313 | + |
| 314 | +## Implementation History |
| 315 | + |
| 316 | +- [x] 02/19/2020: Proposed idea in an issue or [community meeting] |
| 317 | +- [x] 02/24/2020: Proposal submitted to `controller-runtime` |
| 318 | +- [x] 03/02/2020: Updated with default `DefaultControllerConfiguration` |
| 319 | + |
| 320 | + |
| 321 | +<!-- Links --> |
| 322 | +[community meeting]: https://docs.google.com/document/d/1Ih-2cgg1bUrLwLVTB9tADlPcVdgnuMNBGbUl4D-0TIk |
0 commit comments