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