github.com/dmvolod/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