Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Allowing context to be used with client-go #1166

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions contributors/design-proposals/api-machinery/client-go-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Using context.Context with client-go

* Authors: @maleck13

## Abstract

This proposal aims to outline how to allow using the [context package](https://golang.org/pkg/context/) with [client-go](https://github.com/kubernetes/client-go) to allow propagation of cancellation and timeouts
and also to allow exposing context based http tracing [go blog on context based httptrace](https://blog.golang.org/http-tracing) to consumers of the client.

## Motivation and Goals

When using client-go, external calls to the Kubernetes API are made in order to manage resources. The initiator of these calls may find due to an external event,
that they no longer need the resource they had requested, but they have no means by which to inform the client of this change.

When using client-go, having the ability to add context based http tracing would be valuable for issues around performance, observability and debugging.

- Allow consumers of the client to indicate, in an idiomatic way, that the action that caused them to invoke the client has been cancelled.
- Allow consumers of the client to instrument and gain insight via context based http tracing.

## Non Goals

- Introduce a transactional system for write operations whereby upon cancellation the client would somehow rollback any current write operations.

## API

There are two approaches outlined. The first is a non breaking change that appends to the existing API for resources but perhaps puts more burden on the client consumer.
The second is a breaking change to the client-go api, that is potentially more idiomatic and clear.

Common to both is the fact that the request type in client-go already exposes a ```Context``` function: [request context set on client-go rest.Request](https://github.com/kubernetes/client-go/blob/master/rest/request.go#L393).
If set, this context is passed all the way through to the underlying ```http.Request```: [request context passed to http request code](https://github.com/kubernetes/client-go/blob/master/rest/request.go#L484).
So to achieve the goals, we need only modify the client API to allow passing in an external context that is then used to set the existing context on the request type.

## Chosen Implemenation

### Breaking change

The chosen option is to pass the context with each action on a resource. This may be more idiomatic and explicit:

```go
func (c *pods) Get(ctx context.Context, name string, options meta_v1.GetOptions) (result *v1.Pod, err error) {
result = &v1.Pod{}
err = c.client.Get().
Namespace(c.ns).
Resource("pods").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Context(c.ctx).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line meant to be: Context(ctx)?

Do().
Into(result)
return
}
```

These change would impact on every API. Although the changes required would be many, the simplest approach to maintaining current behavior would be to pass ```context.TODO()```.

For the ```listers``` and ```informers``` the change outlined above (adding new constructors) would still allow a context to be passed through to client functions doing the work.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense in the context of listers and informers to specify a context constructor function?

Some sample use cases for context like tracing and deadlines, won't make much sense to set once per informer or lister, but would make more sense to create a new one every time its needed.

Trace id are commonly set per request and the only thing a deadline set at the construction of the informer or lister would do is limit the time your program can get resources from the api.


**Concerns**

This is very clearly a breaking change that would likely require extensive changes to both tests and implementation code throughout the impacted code bases.

**Initial prototype PR**
https://github.com/kubernetes/kubernetes/pull/58677


## Alternatives considered and rejected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would move the chosen design above this section.


- mailing list discussion https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/kubernetes-sig-apps/xe_ii1iC1r0/oeK0g3ABAwAJ

- twitter poll https://twitter.com/the_sttts/status/965540104470188032

### Non breaking change
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this section to an "alternatives considered an rejected. Link to the mailing list discussion.


The client resources (Pods, Secrets etc...) each expose a resource specific interface ```PodInterface``` for example. This is returned from a ```Getter``` interface:

```go
type PodsGetter interface {
Pods(namespace string) PodInterface
}
```

If we were to modify these resource interfaces and their underlying concrete types ```pod``` in this case, to add a ```WithContext``` method, it would allow consumers of the client to set context while maintaining backwards compatibility for all other consumers of the API.

```go
// PodInterface has methods to work with Pod resources.
type PodInterface interface {
WithContext(ctx context.Context) PodInterface
...
}

type pods struct {
client rest.Interface
ns string
ctx context.Context
}

// WithContext allows you to set a context that will be used by the underlying http request
func (c *pods) WithContext(ctx context.Context) PodInterface {
c.ctx = ctx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithContext should not mutate, but has to create a shallow copy first.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain a little more, do you mean before the assignment on line 60 happens? My understanding is ctx is being passed by value into this method and so is a shallow copy or am I misunderstanding?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I meant a shallow copy of c *pods.

return c
}

```

To pass through this context, it would be necessary to change the underlying client calls to accept the context. Example:

```go
func (c *pods) Get(name string, options meta_v1.GetOptions) (result *v1.Pod, err error) {
result = &v1.Pod{}
err = c.client.Get().
Namespace(c.ns).
Resource("pods").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Context(ctx).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this line meant to be: Context(c.ctx) ?

Do().
Into(result)
return
}
```

This would result in an API that would be interacted with like so if the consumer wished to pass a context:
```go
ctx := req.Context()
pod, err := k8client.CoreV1().Pods(namespace).WithContext(ctx).Get(podName, ...)
```


HTTP Tracing and cancellation are also likely useful when using ```informers``` and ```listers```, as these also use the client interfaces backwards compatibility can be maintained by adding a new set of constructors for adding a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancellation via deadline or timeout in a context would not work well with a single context provided to a long-lived lister/informer. similarly, trace ids are typically per request, not long-lived. I don't think a single context object for a lister/informer is what we want.

specific context object:

```go
func NewConfigMapInformerWithContext(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, ctx context.Context) cache.SharedIndexInformer {}

func (f *configMapInformer) InformerWithContext(ctx context.Context) cache.SharedIndexInformer {}

func defaultConfigMapInformerWithContext(client kubernetes.Interface, resyncPeriod time.Duration, ctx context.Context) cache.SharedIndexInformer {
return NewConfigMapInformerWithContext(client, meta_v1.NamespaceAll, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},ctx)
}

func (f *configMapInformer) ListerWithContext(ctx context.Context) v1.ConfigMapLister {
return v1.NewConfigMapLister(f.InformerWithContext(ctx).GetIndexer())
}
```

**Concerns / Notes**
- The idea behind using a context with a request is it is only meant to live as long as the initial request that created it. If we expose an API like the one outlined, the consumer would need to know that they needed to call
```WithContext(ctx context.Context)``` with each request context.

- The fakes would need to be updated and new tests added to cover the new APIs.

- The number of entry points and maintenance points is increased.