github.skymusic.top/operator-framework/operator-sdk@v0.8.2/doc/user/client.md (about)

     1  # Operator-SDK: Controller Runtime Client API
     2  
     3  ## Overview
     4  
     5  The [`controller-runtime`][repo-controller-runtime] library provides various abstractions to watch and reconcile resources in a Kubernetes cluster via CRUD (Create, Update, Delete, as well as Get and List in this case) operations. Operators use at least one controller to perform a coherent set of tasks within a cluster, usually through a combination of CRUD operations. The Operator SDK uses controller-runtime's [Client][doc-client-client] interface, which provides the interface for these operations.
     6  
     7  controller-runtime defines several interfaces used for cluster interaction:
     8  - `client.Client`: implementers perform CRUD operations on a Kubernetes cluster.
     9  - `manager.Manager`: manages shared dependencies, such as Caches and Clients.
    10  - `reconcile.Reconciler`: compares provided state with actual cluster state and updates the cluster on finding state differences using a Client.
    11  
    12  Clients are the focus of this document. A separate document will discuss Managers.
    13  
    14  ## Client Usage
    15  
    16  ### Default Client
    17  
    18  The SDK relies on a `manager.Manager` to create a `client.Client` interface that performs Create, Update, Delete, Get, and List operations within a `reconcile.Reconciler`'s Reconcile function. The SDK will generate code to create a Manager, which holds a Cache and a Client to be used in CRUD operations and communicate with the API server. By default a Controller's Reconciler will be populated with the Manager's Client which is a [split-client][doc-split-client].
    19  
    20  `pkg/controller/<kind>/<kind>_controller.go`:
    21  ```Go
    22  func newReconciler(mgr manager.Manager) reconcile.Reconciler {
    23  	return &ReconcileKind{client: mgr.GetClient(), scheme: mgr.GetScheme()}
    24  }
    25  
    26  type ReconcileKind struct {
    27  	// Populated above from a manager.Manager.
    28  	client client.Client
    29  	scheme *runtime.Scheme
    30  }
    31  ```
    32  
    33  A split client reads (Get and List) from the Cache and writes (Create, Update, Delete) to the API server. Reading from the Cache significantly reduces request load on the API server; as long as the Cache is updated by the API server, read operations are eventually consistent.
    34  
    35  ### Non-default Client
    36  
    37  An operator developer may wish to create their own Client that serves read requests(Get List) from the API server instead of the cache, for example. controller-runtime provides a [constructor][doc-client-constr] for Clients:
    38  
    39  ```Go
    40  // New returns a new Client using the provided config and Options.
    41  func New(config *rest.Config, options client.Options) (client.Client, error)
    42  ```
    43  
    44  `client.Options` allow the caller to specify how the new Client should communicate with the API server.
    45  
    46  ```Go
    47  // Options are creation options for a Client
    48  type Options struct {
    49  	// Scheme, if provided, will be used to map go structs to GroupVersionKinds
    50  	Scheme *runtime.Scheme
    51  
    52  	// Mapper, if provided, will be used to map GroupVersionKinds to Resources
    53  	Mapper meta.RESTMapper
    54  }
    55  ```
    56  Example:
    57  ```Go
    58  import (
    59      "sigs.k8s.io/controller-runtime/pkg/client/config"
    60      "sigs.k8s.io/controller-runtime/pkg/client"
    61  )
    62  
    63  cfg, err := config.GetConfig()
    64  ...
    65  c, err := client.New(cfg, client.Options{})
    66  ...
    67  ```
    68  
    69  **Note**: defaults are set by `client.New` when Options are empty. The default [scheme][code-scheme-default] will have the [core][doc-k8s-core] Kubernetes resource types registered. The caller *must* set a scheme that has custom operator types registered for the new Client to recognize these types.
    70  
    71  Creating a new Client is not usually necessary nor advised, as the default Client is sufficient for most use cases.
    72  
    73  ### Reconcile and the Client API
    74  
    75  A Reconciler implements the [`reconcile.Reconciler`][doc-reconcile-reconciler] interface, which exposes the Reconcile method. Reconcilers are added to a corresponding Controller for a Kind; Reconcile is called in response to cluster or external Events, with a `reconcile.Request` object argument, to read and write cluster state by the Controller, and returns a `reconcile.Result`. SDK Reconcilers have access to a Client in order to make Kubernetes API calls.
    76  
    77  **Note**: For those familiar with the SDK's old project semantics, [Handle][doc-osdk-handle] received resource events and reconciled state for multiple resource types, whereas Reconcile receives resource events and reconciles state for a single resource type.
    78  
    79  ```Go
    80  // ReconcileKind reconciles a Kind object
    81  type ReconcileKind struct {
    82  	// client, initialized using mgr.Client() above, is a split client
    83  	// that reads objects from the cache and writes to the apiserver
    84  	client client.Client
    85  
    86  	// scheme defines methods for serializing and deserializing API objects,
    87  	// a type registry for converting group, version, and kind information
    88  	// to and from Go schemas, and mappings between Go schemas of different
    89  	// versions. A scheme is the foundation for a versioned API and versioned
    90  	// configuration over time.
    91  	scheme *runtime.Scheme
    92  }
    93  
    94  // Reconcile watches for Events and reconciles cluster state with desired
    95  // state defined in the method body.
    96  // The Controller will requeue the Request to be processed again if an error
    97  // is non-nil or Result.Requeue is true, otherwise upon completion it will
    98  // remove the work from the queue.
    99  func (r *ReconcileKind) Reconcile(request reconcile.Request) (reconcile.Result, error)
   100  ```
   101  
   102  Reconcile is where Controller business logic lives, i.e. where Client API calls are made via `ReconcileKind.client`. A `client.Client` implementer performs the following operations:
   103  
   104  #### Get
   105  
   106  ```Go
   107  // Get retrieves an API object for a given object key from the Kubernetes cluster
   108  // and stores it in obj.
   109  func (c Client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error
   110  ```
   111  **Note**: An `ObjectKey` is simply a `client` package alias for [`types.NamespacedName`][doc-types-nsname].
   112  
   113  Example:
   114  ```Go
   115  import (
   116  	"context"
   117  	"github.com/example-org/app-operator/pkg/apis/cache/v1alpha1"
   118  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
   119  )
   120  
   121  func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
   122  	...
   123  	
   124  	app := &v1alpha1.App{}
   125  	ctx := context.TODO()
   126  	err := r.client.Get(ctx, request.NamespacedName, app)
   127  
   128  	...
   129  }
   130  ```
   131  
   132  #### List
   133  
   134  ```Go
   135  // List retrieves a list of objects for a given namespace and list options
   136  // and stores the list in obj.
   137  func (c Client) List(ctx context.Context, opts *ListOptions, obj runtime.Object) error
   138  ```
   139  A `client.ListOptions` sets filters and options for a `List` call:
   140  ```Go
   141  type ListOptions struct {
   142      // LabelSelector filters results by label.  Use SetLabelSelector to
   143      // set from raw string form.
   144      LabelSelector labels.Selector
   145  
   146      // FieldSelector filters results by a particular field.  In order
   147      // to use this with cache-based implementations, restrict usage to
   148      // a single field-value pair that's been added to the indexers.
   149      FieldSelector fields.Selector
   150  
   151      // Namespace represents the namespace to list for, or empty for
   152      // non-namespaced objects, or to list across all namespaces.
   153      Namespace string
   154  
   155      // Raw represents raw ListOptions, as passed to the API server.  Note
   156      // that these may not be respected by all implementations of interface,
   157      // and the LabelSelector and FieldSelector fields are ignored.
   158      Raw *metav1.ListOptions
   159  }
   160  ```
   161  Example:
   162  ```Go
   163  import (
   164  	"context"
   165  	"fmt"
   166  	"k8s.io/api/core/v1"
   167  	"sigs.k8s.io/controller-runtime/pkg/client"
   168  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
   169  )
   170  
   171  func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
   172  	...
   173  
   174  	// Return all pods in the request namespace with a label of `app=<name>`
   175  	opts := &client.ListOptions{}
   176  	opts.SetLabelSelector(fmt.Sprintf("app=%s", request.NamespacedName.Name))
   177  	opts.InNamespace(request.NamespacedName.Namespace)
   178  
   179  	podList := &v1.PodList{}
   180  	ctx := context.TODO()
   181  	err := r.client.List(ctx, opts, podList)
   182  
   183  	...
   184  }
   185  ```
   186  
   187  #### Create
   188  
   189  ```Go
   190  // Create saves the object obj in the Kubernetes cluster.
   191  // Returns an error
   192  func (c Client) Create(ctx context.Context, obj runtime.Object) error
   193  ```
   194  Example:
   195  ```Go
   196  import (
   197  	"context"
   198  	"k8s.io/api/apps/v1"
   199  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
   200  )
   201  
   202  func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
   203  	...
   204  
   205  	app := &v1.Deployment{ // Any cluster object you want to create.
   206  		...
   207  	}
   208  	ctx := context.TODO()
   209  	err := r.client.Create(ctx, app)
   210  
   211  	...
   212  }
   213  ```
   214  
   215  #### Update
   216  
   217  ```Go
   218  // Update updates the given obj in the Kubernetes cluster. obj must be a
   219  // struct pointer so that obj can be updated with the content returned
   220  // by the API server. Update does *not* update the resource's status
   221  // subresource
   222  func (c Client) Update(ctx context.Context, obj runtime.Object) error
   223  ```
   224  Example:
   225  ```Go
   226  import (
   227  	"context"
   228  	"k8s.io/api/apps/v1"
   229  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
   230  )
   231  
   232  func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
   233  	...
   234  	
   235  	dep := &v1.Deployment{}
   236  	err := r.client.Get(context.TODO(), request.NamespacedName, dep)
   237  
   238  	...
   239  
   240  	ctx := context.TODO()
   241  	dep.Spec.Selector.MatchLabels["is_running"] = "true"
   242  	err := r.client.Update(ctx, dep)
   243  
   244  	...
   245  }
   246  ```
   247  
   248  ##### Updating Status Subresource
   249  
   250  When updating the [status subresource][cr-status-subresource] from the client,
   251  the StatusWriter must be used which can be gotten with `Status()`
   252  
   253  ##### Status
   254  
   255  ```Go
   256  // Status() returns a StatusWriter object that can be used to update the
   257  // object's status subresource
   258  func (c Client) Status() (client.StatusWriter, error)
   259  ```
   260  
   261  Example:
   262  ```Go
   263  import (
   264  	"context"
   265  	cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1"
   266  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
   267  )
   268  
   269  func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
   270  	...
   271  	
   272  	mem := &cachev1alpha1.Memcached{}
   273  	err := r.client.Get(context.TODO(), request.NamespacedName, mem)
   274  
   275  	...
   276  
   277  	ctx := context.TODO()
   278  	mem.Status.Nodes = []string{"pod1", "pod2"}
   279  	err := r.client.Status().Update(ctx, mem)
   280  
   281  	...
   282  }
   283  ```
   284  
   285  
   286  #### Delete
   287  
   288  ```Go
   289  // Delete deletes the given obj from Kubernetes cluster.
   290  func (c Client) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error
   291  ```
   292  A `client.DeleteOptionFunc` sets fields of `client.DeleteOptions` to configure a `Delete` call:
   293  ```Go
   294  // DeleteOptionFunc is a function that mutates a DeleteOptions struct.
   295  type DeleteOptionFunc func(*DeleteOptions)
   296  
   297  type DeleteOptions struct {
   298      // GracePeriodSeconds is the duration in seconds before the object should be
   299      // deleted. Value must be non-negative integer. The value zero indicates
   300      // delete immediately. If this value is nil, the default grace period for the
   301      // specified type will be used.
   302      GracePeriodSeconds *int64
   303  
   304      // Preconditions must be fulfilled before a deletion is carried out. If not
   305      // possible, a 409 Conflict status will be returned.
   306      Preconditions *metav1.Preconditions
   307  
   308      // PropagationPolicy determined whether and how garbage collection will be
   309      // performed. Either this field or OrphanDependents may be set, but not both.
   310      // The default policy is decided by the existing finalizer set in the
   311      // metadata.finalizers and the resource-specific default policy.
   312      // Acceptable values are: 'Orphan' - orphan the dependents; 'Background' -
   313      // allow the garbage collector to delete the dependents in the background;
   314      // 'Foreground' - a cascading policy that deletes all dependents in the
   315      // foreground.
   316      PropagationPolicy *metav1.DeletionPropagation
   317  
   318      // Raw represents raw DeleteOptions, as passed to the API server.
   319      Raw *metav1.DeleteOptions
   320  }
   321  ```
   322  Example:
   323  ```Go
   324  import (
   325  	"context"
   326  	"k8s.io/api/core/v1"
   327  	"sigs.k8s.io/controller-runtime/pkg/client"
   328  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
   329  )
   330  
   331  func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
   332  	...
   333  	
   334  	pod := &v1.Pod{}
   335  	err := r.client.Get(context.TODO(), request.NamespacedName, pod)
   336  
   337  	...
   338  
   339  	ctx := context.TODO()
   340  	if pod.Status.Phase == v1.PodUnknown {
   341  		// Delete the pod after 5 seconds.
   342  		err := r.client.Delete(ctx, pod, client.GracePeriodSeconds(5))
   343  		...
   344  	}
   345  
   346  	...
   347  }
   348  ```
   349  
   350  ### Example usage
   351  
   352  ```Go
   353  import (
   354  	"context"
   355  	"reflect"
   356  
   357  	appv1alpha1 "github.com/example-org/app-operator/pkg/apis/app/v1alpha1"
   358  
   359  	appsv1 "k8s.io/api/apps/v1"
   360  	corev1 "k8s.io/api/core/v1"
   361  	"k8s.io/apimachinery/pkg/api/errors"
   362  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
   363  	"k8s.io/apimachinery/pkg/labels"
   364  	"k8s.io/apimachinery/pkg/runtime"
   365  	"k8s.io/apimachinery/pkg/types"
   366  	"sigs.k8s.io/controller-runtime/pkg/client"
   367  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
   368  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
   369  )
   370  
   371  type ReconcileApp struct {
   372  	client client.Client
   373  	scheme *runtime.Scheme
   374  }
   375  
   376  func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
   377  
   378  	// Fetch the App instance.
   379  	app := &appv1alpha1.App{}
   380  	err := r.client.Get(context.TODO(), request.NamespacedName, app)
   381  	if err != nil {
   382  		if errors.IsNotFound(err) {
   383  			return reconcile.Result{}, nil
   384  		}
   385  		return reconcile.Result{}, err
   386  	}
   387  
   388  	// Check if the deployment already exists, if not create a new deployment.
   389  	found := &appsv1.Deployment{}
   390  	err = r.client.Get(context.TODO(), types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, found)
   391  	if err != nil {
   392  	 	if errors.IsNotFound(err) {
   393  			// Define and create a new deployment.
   394  			dep := r.deploymentForApp(app)
   395  			if err = r.client.Create(context.TODO(), dep); err != nil {
   396  				return reconcile.Result{}, err
   397  			}
   398  			return reconcile.Result{Requeue: true}, nil
   399  		} else {
   400  			return reconcile.Result{}, err			
   401  		}
   402  	}
   403  
   404  	// Ensure the deployment size is the same as the spec.
   405  	size := app.Spec.Size
   406  	if *found.Spec.Replicas != size {
   407  		found.Spec.Replicas = &size
   408  		if err = r.client.Update(context.TODO(), found); err != nil {
   409  			return reconcile.Result{}, err
   410  		}
   411  		return reconcile.Result{Requeue: true}, nil
   412  	}
   413  
   414  	// Update the App status with the pod names.
   415  	// List the pods for this app's deployment.
   416  	podList := &corev1.PodList{}
   417  	labelSelector := labels.SelectorFromSet(labelsForApp(app.Name))
   418  	listOps := &client.ListOptions{Namespace: app.Namespace, LabelSelector: labelSelector}
   419  	if err = r.client.List(context.TODO(), listOps, podList); err != nil {
   420  		return reconcile.Result{}, err
   421  	}
   422  
   423  	// Update status.Nodes if needed.
   424  	podNames := getPodNames(podList.Items)
   425  	if !reflect.DeepEqual(podNames, app.Status.Nodes) {
   426  		app.Status.Nodes = podNames
   427  		if err := r.client.Status().Update(context.TODO(), app); err != nil {
   428  			return reconcile.Result{}, err
   429  		}
   430  	}
   431  
   432  	return reconcile.Result{}, nil
   433  }
   434  
   435  // deploymentForApp returns a app Deployment object.
   436  func (r *ReconcileKind) deploymentForApp(m *appv1alpha1.App) *appsv1.Deployment {
   437  	lbls := labelsForApp(m.Name)
   438  	replicas := m.Spec.Size
   439  
   440  	dep := &appsv1.Deployment{
   441  		TypeMeta: metav1.TypeMeta{
   442  			APIVersion: "apps/v1",
   443  			Kind:       "Deployment",
   444  		},
   445  		ObjectMeta: metav1.ObjectMeta{
   446  			Name:      m.Name,
   447  			Namespace: m.Namespace,
   448  		},
   449  		Spec: appsv1.DeploymentSpec{
   450  			Replicas: &replicas,
   451  			Selector: &metav1.LabelSelector{
   452  				MatchLabels: lbls,
   453  			},
   454  			Template: corev1.PodTemplateSpec{
   455  				ObjectMeta: metav1.ObjectMeta{
   456  					Labels: lbls,
   457  				},
   458  				Spec: corev1.PodSpec{
   459  					Containers: []corev1.Container{{
   460  						Image:   "app:alpine",
   461  						Name:    "app",
   462  						Command: []string{"app", "-a=64", "-b"},
   463  						Ports: []corev1.ContainerPort{{
   464  							ContainerPort: 10000,
   465  							Name:          "app",
   466  						}},
   467  					}},
   468  				},
   469  			},
   470  		},
   471  	}
   472  
   473  	// Set App instance as the owner and controller.
   474  	// NOTE: calling SetControllerReference, and setting owner references in
   475  	// general, is important as it allows deleted objects to be garbage collected.
   476  	controllerutil.SetControllerReference(m, dep, r.scheme)
   477  	return dep
   478  }
   479  
   480  // labelsForApp creates a simple set of labels for App.
   481  func labelsForApp(name string) map[string]string {
   482  	return map[string]string{"app_name": "app", "app_cr": name}
   483  }
   484  ```
   485  
   486  [repo-controller-runtime]:https://github.com/kubernetes-sigs/controller-runtime
   487  [doc-client-client]:https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/client#Client
   488  [doc-split-client]:https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/client#DelegatingClient
   489  [doc-client-constr]:https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/client#New
   490  [code-scheme-default]:https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/client/client.go#L51
   491  [doc-k8s-core]:https://godoc.org/k8s.io/api/core/v1
   492  [doc-reconcile-reconciler]:https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/reconcile#Reconciler
   493  [doc-osdk-handle]:https://github.com/operator-framework/operator-sdk/blob/master/doc/design/milestone-0.0.2/action-api.md#handler
   494  [doc-types-nsname]:https://godoc.org/k8s.io/apimachinery/pkg/types#NamespacedName
   495  [cr-status-subresource]:https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#status-subresource