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