github.skymusic.top/operator-framework/operator-sdk@v0.8.2/doc/user-guide.md (about) 1 # User Guide 2 3 This guide walks through an example of building a simple memcached-operator using the operator-sdk CLI tool and controller-runtime library API. To learn how to use Ansible or Helm to create an operator, see the [Ansible Operator User Guide][ansible_user_guide] or the [Helm Operator User Guide][helm_user_guide]. The rest of this document will show how to program an operator in Go. 4 5 6 ## Prerequisites 7 8 - [dep][dep_tool] version v0.5.0+. 9 - [git][git_tool] 10 - [go][go_tool] version v1.12+. 11 - [docker][docker_tool] version 17.03+. 12 - [kubectl][kubectl_tool] version v1.11.3+. 13 - Access to a Kubernetes v1.11.3+ cluster. 14 15 **Note**: This guide uses [minikube][minikube_tool] version v0.25.0+ as the local Kubernetes cluster and [quay.io][quay_link] for the public registry. 16 17 ## Install the Operator SDK CLI 18 19 Follow the steps in the [installation guide][install_guide] to learn how to install the Operator SDK CLI tool. 20 21 ## Create a new project 22 23 Use the CLI to create a new memcached-operator project: 24 25 ```sh 26 $ mkdir -p $GOPATH/src/github.com/example-inc/ 27 $ cd $GOPATH/src/github.com/example-inc/ 28 $ operator-sdk new memcached-operator 29 $ cd memcached-operator 30 ``` 31 32 To learn about the project directory structure, see [project layout][layout_doc] doc. 33 34 #### A note on dependency management 35 36 By default, `operator-sdk new` generates a `go.mod` file to be used with [Go modules][go_mod_wiki]. If you'd like to use [`dep`][dep_tool], set `--dep-manager=dep` when initializing your project, which will create a `Gopkg.toml` file with the same dependency information. 37 38 ##### Go modules 39 40 If using go modules (the default dependency manager) in your project, ensure you activate module support before using the SDK. From the [go modules Wiki][go_mod_wiki]: 41 42 > You can activate module support in one of two ways: 43 > - Invoke the go command in a directory outside of the $GOPATH/src tree, with a valid go.mod file in the current directory or any parent of it and the environment variable GO111MODULE unset (or explicitly set to auto). 44 > - Invoke the go command with GO111MODULE=on environment variable set. 45 46 As of now, the SDK only supports initializing new projects in `$GOPATH/src`. We intend to support all go module modes for projects in the near future. 47 48 You can set `GO111MODULE` in your CLI to activate currently supported behavior by running the following command: 49 50 ```sh 51 $ export GO111MODULE=on 52 ``` 53 54 ##### Vendoring 55 56 The Operator SDK uses [vendoring][go_vendoring] to supply dependencies to operator projects, regardless of the dependency manager. As with the above module mode constraint, we intend to allow use of dependencies [outside of `vendor`][module_vendoring] for projects in the near future. 57 58 #### Operator scope 59 60 Read the [operator scope][operator_scope] documentation on how to run your operator as namespace-scoped vs cluster-scoped. 61 62 ### Manager 63 The main program for the operator `cmd/manager/main.go` initializes and runs the [Manager][manager_go_doc]. 64 65 The Manager will automatically register the scheme for all custom resources defined under `pkg/apis/...` and run all controllers under `pkg/controller/...`. 66 67 The Manager can restrict the namespace that all controllers will watch for resources: 68 ```Go 69 mgr, err := manager.New(cfg, manager.Options{Namespace: namespace}) 70 ``` 71 By default this will be the namespace that the operator is running in. To watch all namespaces leave the namespace option empty: 72 ```Go 73 mgr, err := manager.New(cfg, manager.Options{Namespace: ""}) 74 ``` 75 76 ## Add a new Custom Resource Definition 77 78 Add a new Custom Resource Definition(CRD) API called Memcached, with APIVersion `cache.example.com/v1alpha1` and Kind `Memcached`. 79 80 ```sh 81 $ operator-sdk add api --api-version=cache.example.com/v1alpha1 --kind=Memcached 82 ``` 83 84 This will scaffold the Memcached resource API under `pkg/apis/cache/v1alpha1/...`. 85 86 ### Define the spec and status 87 88 Modify the spec and status of the `Memcached` Custom Resource(CR) at `pkg/apis/cache/v1alpha1/memcached_types.go`: 89 90 ```Go 91 type MemcachedSpec struct { 92 // Size is the size of the memcached deployment 93 Size int32 `json:"size"` 94 } 95 type MemcachedStatus struct { 96 // Nodes are the names of the memcached pods 97 Nodes []string `json:"nodes"` 98 } 99 ``` 100 101 After modifying the `*_types.go` file always run the following command to update the generated code for that resource type: 102 103 ```sh 104 $ operator-sdk generate k8s 105 ``` 106 107 ## Add a new Controller 108 109 Add a new [Controller][controller-go-doc] to the project that will watch and reconcile the Memcached resource: 110 111 ```sh 112 $ operator-sdk add controller --api-version=cache.example.com/v1alpha1 --kind=Memcached 113 ``` 114 115 This will scaffold a new Controller implementation under `pkg/controller/memcached/...`. 116 117 For this example replace the generated Controller file `pkg/controller/memcached/memcached_controller.go` with the example [`memcached_controller.go`][memcached_controller] implementation. 118 119 The example Controller executes the following reconciliation logic for each `Memcached` CR: 120 - Create a memcached Deployment if it doesn't exist 121 - Ensure that the Deployment size is the same as specified by the `Memcached` CR spec 122 - Update the `Memcached` CR status using the status writer with the names of the memcached pods 123 124 The next two subsections explain how the Controller watches resources and how the reconcile loop is triggered. Skip to the [Build](#build-and-run-the-operator) section to see how to build and run the operator. 125 126 ### Resources watched by the Controller 127 128 Inspect the Controller implementation at `pkg/controller/memcached/memcached_controller.go` to see how the Controller watches resources. 129 130 The first watch is for the Memcached type as the primary resource. For each Add/Update/Delete event the reconcile loop will be sent a reconcile `Request` (a namespace/name key) for that Memcached object: 131 132 ```Go 133 err := c.Watch( 134 &source.Kind{Type: &cachev1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{}) 135 ``` 136 137 The next watch is for Deployments but the event handler will map each event to a reconcile `Request` for the owner of the Deployment. Which in this case is the Memcached object for which the Deployment was created. This allows the controller to watch Deployments as a secondary resource. 138 139 ```Go 140 err := c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{ 141 IsController: true, 142 OwnerType: &cachev1alpha1.Memcached{}, 143 }) 144 ``` 145 146 **// TODO:** Doc on eventhandler, arbitrary mapping between watched and reconciled resource. 147 148 **// TODO:** Doc on configuring a Controller: number of workers, predicates, watching channels, 149 150 ### Reconcile loop 151 152 Every Controller has a Reconciler object with a `Reconcile()` method that implements the reconcile loop. The reconcile loop is passed the [`Request`][request-go-doc] argument which is a Namespace/Name key used to lookup the primary resource object, Memcached, from the cache: 153 154 ```Go 155 func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error) { 156 // Lookup the Memcached instance for this reconcile request 157 memcached := &cachev1alpha1.Memcached{} 158 err := r.client.Get(context.TODO(), request.NamespacedName, memcached) 159 ... 160 } 161 ``` 162 163 Based on the return values, [`Result`][result_go_doc] and error, the `Request` may be requeued and the reconcile loop may be triggered again: 164 165 ```Go 166 // Reconcile successful - don't requeue 167 return reconcile.Result{}, nil 168 // Reconcile failed due to error - requeue 169 return reconcile.Result{}, err 170 // Requeue for any reason other than error 171 return reconcile.Result{Requeue: true}, nil 172 ``` 173 174 You can set the `Result.RequeueAfter` to requeue the `Request` after a grace period as well: 175 ```Go 176 import "time" 177 178 // Reconcile for any reason than error after 5 seconds 179 return reconcile.Result{RequeueAfter: time.Second*5}, nil 180 ``` 181 182 **Note:** Returning `Result` with `RequeueAfter` set is how you can periodically reconcile a CR. 183 184 For a guide on Reconcilers, Clients, and interacting with resource Events, see the [Client API doc][doc_client_api]. 185 186 ## Build and run the operator 187 188 Before running the operator, the CRD must be registered with the Kubernetes apiserver: 189 190 ```sh 191 $ kubectl create -f deploy/crds/cache_v1alpha1_memcached_crd.yaml 192 ``` 193 194 Once this is done, there are two ways to run the operator: 195 196 - As a Deployment inside a Kubernetes cluster 197 - As Go program outside a cluster 198 199 ### 1. Run as a Deployment inside the cluster 200 201 **Note**: `operator-sdk build` invokes `docker build` by default, and optionally `buildah bud`. If using `buildah`, skip to the `operator-sdk build` invocation instructions below. If using `docker`, make sure your docker daemon is running and that you can run the docker client without sudo. You can check if this is the case by running `docker version`, which should complete without errors. Follow instructions for your OS/distribution on how to start the docker daemon and configure your access permissions, if needed. 202 203 **Note**: If using go modules, run 204 ```sh 205 $ go mod vendor 206 ``` 207 before building the memcached-operator image. 208 209 Build the memcached-operator image and push it to a registry: 210 ```sh 211 $ operator-sdk build quay.io/example/memcached-operator:v0.0.1 212 $ sed -i 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml 213 $ docker push quay.io/example/memcached-operator:v0.0.1 214 ``` 215 216 If you created your operator using `--cluster-scoped=true`, update the service account namespace in the generated `ClusterRoleBinding` to match where you are deploying your operator. 217 ```sh 218 $ export OPERATOR_NAMESPACE=$(kubectl config view --minify -o jsonpath='{.contexts[0].context.namespace}') 219 $ sed -i "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml 220 ``` 221 222 **Note** 223 If you are performing these steps on OSX, use the following commands instead: 224 ```sh 225 $ sed -i "" 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml 226 $ sed -i "" "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml 227 ``` 228 229 The Deployment manifest is generated at `deploy/operator.yaml`. Be sure to update the deployment image as shown above since the default is just a placeholder. 230 231 Setup RBAC and deploy the memcached-operator: 232 233 ```sh 234 $ kubectl create -f deploy/service_account.yaml 235 $ kubectl create -f deploy/role.yaml 236 $ kubectl create -f deploy/role_binding.yaml 237 $ kubectl create -f deploy/operator.yaml 238 ``` 239 240 Verify that the memcached-operator is up and running: 241 242 ```sh 243 $ kubectl get deployment 244 NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 245 memcached-operator 1 1 1 1 1m 246 ``` 247 248 ### 2. Run locally outside the cluster 249 250 This method is preferred during development cycle to deploy and test faster. 251 252 Set the name of the operator in an environment variable: 253 254 ```sh 255 export OPERATOR_NAME=memcached-operator 256 ``` 257 258 Run the operator locally with the default Kubernetes config file present at `$HOME/.kube/config`: 259 260 ```sh 261 $ operator-sdk up local --namespace=default 262 2018/09/30 23:10:11 Go Version: go1.10.2 263 2018/09/30 23:10:11 Go OS/Arch: darwin/amd64 264 2018/09/30 23:10:11 operator-sdk Version: 0.0.6+git 265 2018/09/30 23:10:12 Registering Components. 266 2018/09/30 23:10:12 Starting the Cmd. 267 ``` 268 269 You can use a specific kubeconfig via the flag `--kubeconfig=<path/to/kubeconfig>`. 270 271 ## Create a Memcached CR 272 273 Create the example `Memcached` CR that was generated at `deploy/crds/cache_v1alpha1_memcached_cr.yaml`: 274 275 ```sh 276 $ cat deploy/crds/cache_v1alpha1_memcached_cr.yaml 277 apiVersion: "cache.example.com/v1alpha1" 278 kind: "Memcached" 279 metadata: 280 name: "example-memcached" 281 spec: 282 size: 3 283 284 $ kubectl apply -f deploy/crds/cache_v1alpha1_memcached_cr.yaml 285 ``` 286 287 Ensure that the memcached-operator creates the deployment for the CR: 288 289 ```sh 290 $ kubectl get deployment 291 NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 292 memcached-operator 1 1 1 1 2m 293 example-memcached 3 3 3 3 1m 294 ``` 295 296 Check the pods and CR status to confirm the status is updated with the memcached pod names: 297 298 ```sh 299 $ kubectl get pods 300 NAME READY STATUS RESTARTS AGE 301 example-memcached-6fd7c98d8-7dqdr 1/1 Running 0 1m 302 example-memcached-6fd7c98d8-g5k7v 1/1 Running 0 1m 303 example-memcached-6fd7c98d8-m7vn7 1/1 Running 0 1m 304 memcached-operator-7cc7cfdf86-vvjqk 1/1 Running 0 2m 305 ``` 306 307 ```sh 308 $ kubectl get memcached/example-memcached -o yaml 309 apiVersion: cache.example.com/v1alpha1 310 kind: Memcached 311 metadata: 312 clusterName: "" 313 creationTimestamp: 2018-03-31T22:51:08Z 314 generation: 0 315 name: example-memcached 316 namespace: default 317 resourceVersion: "245453" 318 selfLink: /apis/cache.example.com/v1alpha1/namespaces/default/memcacheds/example-memcached 319 uid: 0026cc97-3536-11e8-bd83-0800274106a1 320 spec: 321 size: 3 322 status: 323 nodes: 324 - example-memcached-6fd7c98d8-7dqdr 325 - example-memcached-6fd7c98d8-g5k7v 326 - example-memcached-6fd7c98d8-m7vn7 327 ``` 328 329 ### Update the size 330 331 Change the `spec.size` field in the memcached CR from 3 to 4 and apply the change: 332 333 ```sh 334 $ cat deploy/crds/cache_v1alpha1_memcached_cr.yaml 335 apiVersion: "cache.example.com/v1alpha1" 336 kind: "Memcached" 337 metadata: 338 name: "example-memcached" 339 spec: 340 size: 4 341 342 $ kubectl apply -f deploy/crds/cache_v1alpha1_memcached_cr.yaml 343 ``` 344 345 Confirm that the operator changes the deployment size: 346 347 ```sh 348 $ kubectl get deployment 349 NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 350 example-memcached 4 4 4 4 5m 351 ``` 352 353 ### Cleanup 354 355 Clean up the resources: 356 357 ```sh 358 $ kubectl delete -f deploy/crds/cache_v1alpha1_memcached_cr.yaml 359 $ kubectl delete -f deploy/operator.yaml 360 $ kubectl delete -f deploy/role_binding.yaml 361 $ kubectl delete -f deploy/role.yaml 362 $ kubectl delete -f deploy/service_account.yaml 363 ``` 364 365 ## Advanced Topics 366 367 ### Adding 3rd Party Resources To Your Operator 368 369 The operator's Manager supports the Core Kubernetes resource types as found in the client-go [scheme][scheme_package] package and will also register the schemes of all custom resource types defined in your project under `pkg/apis`. 370 371 ```Go 372 import ( 373 "github.com/example-inc/memcached-operator/pkg/apis" 374 ... 375 ) 376 377 // Setup Scheme for all resources 378 if err := apis.AddToScheme(mgr.GetScheme()); err != nil { 379 log.Error(err, "") 380 os.Exit(1) 381 } 382 ``` 383 384 To add a 3rd party resource to an operator, you must add it to the Manager's scheme. By creating an `AddToScheme()` method or reusing one you can easily add a resource to your scheme. An [example][deployments_register] shows that you define a function and then use the [runtime][runtime_package] package to create a `SchemeBuilder`. 385 386 #### Register with the Manager's scheme 387 388 Call the `AddToScheme()` function for your 3rd party resource and pass it the Manager's scheme via `mgr.GetScheme()`. 389 390 Example: 391 ```go 392 import ( 393 .... 394 395 routev1 "github.com/openshift/api/route/v1" 396 ) 397 398 func main() { 399 .... 400 401 // Adding the routev1 402 if err := routev1.AddToScheme(mgr.GetScheme()); err != nil { 403 log.Error(err, "") 404 os.Exit(1) 405 } 406 407 .... 408 409 // Setup all Controllers 410 if err := controller.AddToManager(mgr); err != nil { 411 log.Error(err, "") 412 os.Exit(1) 413 } 414 } 415 ``` 416 417 **NOTES:** 418 419 * After adding new import paths to your operator project, run `go mod vendor` (or `dep ensure` if you set `--dep-manager=dep` when initializing your project) in the root of your project directory to fulfill these dependencies. 420 * Your 3rd party resource needs to be added before add the controller in `"Setup all Controllers"`. 421 422 ### Handle Cleanup on Deletion 423 424 To implement complex deletion logic, you can add a finalizer to your Custom Resource. This will prevent your Custom Resource from being 425 deleted until you remove the finalizer (ie, after your cleanup logic has successfully run). For more information, see the 426 [official Kubernetes documentation on finalizers](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers). 427 428 **Example:** 429 430 The following is a snippet from the controller file under `pkg/controller/memcached/memcached_controller.go` 431 432 ```Go 433 func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error) { 434 reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 435 reqLogger.Info("Reconciling Memcached") 436 437 // Fetch the Memcached instance 438 memcached := &cachev1alpha1.Memcached{} 439 err := r.client.Get(context.TODO(), request.NamespacedName, memcached) 440 ... 441 // Check if the APP CR was marked to be deleted 442 isMemcachedMarkedToBeDeleted := memcached.GetDeletionTimestamp() != nil 443 if isMemcachedMarkedToBeDeleted { 444 // TODO(user): Add the cleanup steps that the operator needs to do before the CR can be deleted 445 // Update finalizer to allow delete CR 446 memcached.SetFinalizers(nil) 447 448 // Update CR 449 err := r.client.Update(context.TODO(), memcached) 450 if err != nil { 451 return reconcile.Result{}, err 452 } 453 return reconcile.Result{}, nil 454 } 455 456 // Add finalizer for this CR 457 if err := r.addFinalizer(reqLogger, instance); err != nil { 458 return reconcile.Result{}, err 459 } 460 ... 461 462 return reconcile.Result{}, nil 463 } 464 465 //addFinalizer will add this attribute to the Memcached CR 466 func (r *ReconcileMemcached) addFinalizer(reqLogger logr.Logger, m *cachev1alpha1.Memcached) error { 467 if len(m.GetFinalizers()) < 1 && m.GetDeletionTimestamp() == nil { 468 reqLogger.Info("Adding Finalizer for the Memcached") 469 m.SetFinalizers([]string{"finalizer.cache.example.com"}) 470 471 // Update CR 472 err := r.client.Update(context.TODO(), m) 473 if err != nil { 474 reqLogger.Error(err, "Failed to update Memcached with finalizer") 475 return err 476 } 477 } 478 return nil 479 } 480 481 ``` 482 483 ### Metrics 484 485 To learn about how metrics work in the Operator SDK read the [metrics section][metrics_doc] of the user documentation. 486 487 ## Leader election 488 489 During the lifecycle of an operator it's possible that there may be more than 1 instance running at any given time e.g when rolling out an upgrade for the operator. 490 In such a scenario it is necessary to avoid contention between multiple operator instances via leader election so that only one leader instance handles the reconciliation while the other instances are inactive but ready to take over when the leader steps down. 491 492 There are two different leader election implementations to choose from, each with its own tradeoff. 493 494 - [Leader-for-life][leader_for_life]: The leader pod only gives up leadership (via garbage collection) when it is deleted. This implementation precludes the possibility of 2 instances mistakenly running as leaders (split brain). However, this method can be subject to a delay in electing a new leader. For instance when the leader pod is on an unresponsive or partitioned node, the [`pod-eviction-timeout`][pod_eviction_timeout] dictates how it takes for the leader pod to be deleted from the node and step down (default 5m). 495 - [Leader-with-lease][leader_with_lease]: The leader pod periodically renews the leader lease and gives up leadership when it can't renew the lease. This implementation allows for a faster transition to a new leader when the existing leader is isolated, but there is a possibility of split brain in [certain situations][lease_split_brain]. 496 497 By default the SDK enables the leader-for-life implementation. However you should consult the docs above for both approaches to consider the tradeoffs that make sense for your use case. 498 499 The following examples illustrate how to use the two options: 500 501 ### Leader for life 502 503 A call to `leader.Become()` will block the operator as it retries until it can become the leader by creating the configmap named `memcached-operator-lock`. 504 505 ```Go 506 import ( 507 ... 508 "github.com/operator-framework/operator-sdk/pkg/leader" 509 ) 510 511 func main() { 512 ... 513 err = leader.Become(context.TODO(), "memcached-operator-lock") 514 if err != nil { 515 log.Error(err, "Failed to retry for leader lock") 516 os.Exit(1) 517 } 518 ... 519 } 520 ``` 521 If the operator is not running inside a cluster `leader.Become()` will simply return without error to skip the leader election since it can't detect the operator's namespace. 522 523 ### Leader with lease 524 525 The leader-with-lease approach can be enabled via the [Manager Options][manager_options] for leader election. 526 527 ```Go 528 import ( 529 ... 530 "sigs.k8s.io/controller-runtime/pkg/manager" 531 ) 532 533 func main() { 534 ... 535 opts := manager.Options{ 536 ... 537 LeaderElection: true, 538 LeaderElectionID: "memcached-operator-lock" 539 } 540 mgr, err := manager.New(cfg, opts) 541 ... 542 } 543 ``` 544 545 When the operator is not running in a cluster, the Manager will return an error on starting since it can't detect the operator's namespace in order to create the configmap for leader election. You can override this namespace by setting the Manager's `LeaderElectionNamespace` option. 546 547 [operator_scope]:./operator-scope.md 548 [install_guide]: ./user/install-operator-sdk.md 549 [pod_eviction_timeout]: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/#options 550 [manager_options]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/manager#Options 551 [lease_split_brain]: https://github.com/kubernetes/client-go/blob/30b06a83d67458700a5378239df6b96948cb9160/tools/leaderelection/leaderelection.go#L21-L24 552 [leader_for_life]: https://godoc.org/github.com/operator-framework/operator-sdk/pkg/leader 553 [leader_with_lease]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/leaderelection 554 [memcached_handler]: ../example/memcached-operator/handler.go.tmpl 555 [memcached_controller]: ../example/memcached-operator/memcached_controller.go.tmpl 556 [layout_doc]:./project_layout.md 557 [ansible_user_guide]:./ansible/user-guide.md 558 [helm_user_guide]:./helm/user-guide.md 559 [homebrew_tool]:https://brew.sh/ 560 [go_mod_wiki]: https://github.com/golang/go/wiki/Modules 561 [go_vendoring]: https://blog.gopheracademy.com/advent-2015/vendor-folder/ 562 [module_vendoring]: https://github.com/golang/go/wiki/Modules#how-do-i-use-vendoring-with-modules-is-vendoring-going-away 563 [dep_tool]:https://golang.github.io/dep/docs/installation.html 564 [git_tool]:https://git-scm.com/downloads 565 [go_tool]:https://golang.org/dl/ 566 [docker_tool]:https://docs.docker.com/install/ 567 [kubectl_tool]:https://kubernetes.io/docs/tasks/tools/install-kubectl/ 568 [minikube_tool]:https://github.com/kubernetes/minikube#installation 569 [scheme_package]:https://github.com/kubernetes/client-go/blob/master/kubernetes/scheme/register.go 570 [deployments_register]: https://github.com/kubernetes/api/blob/master/apps/v1/register.go#L41 571 [doc_client_api]:./user/client.md 572 [runtime_package]: https://godoc.org/k8s.io/apimachinery/pkg/runtime 573 [manager_go_doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/manager#Manager 574 [controller-go-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg#hdr-Controller 575 [request-go-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/reconcile#Request 576 [result_go_doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/reconcile#Result 577 [metrics_doc]: ./user/metrics/README.md 578 [quay_link]: https://quay.io