Skip to content

Commit aaf231f

Browse files
authored
Merge pull request kubernetes#2419 from jpbetz/apply-client-kep-conversions
Apply Client KEP: promote to implementable, address community feedback
2 parents 5d98e09 + 4b63b6a commit aaf231f

File tree

3 files changed

+158
-102
lines changed

3 files changed

+158
-102
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kep-number: 2155
2+
stable:
3+
approver: "@deads2k"

keps/sig-api-machinery/2144-clientgo-apply/README.md renamed to keps/sig-api-machinery/2155-clientgo-apply/README.md

Lines changed: 147 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@
1212
- [Design Details](#design-details)
1313
- [Apply functions](#apply-functions)
1414
- [Generated apply configuration types](#generated-apply-configuration-types)
15-
- [Alternative 1: Generated structs where all fields are pointers](#alternative-1-generated-structs-where-all-fields-are-pointers)
16-
- [Alternative 2: Generated "builders"](#alternative-2-generated-builders)
17-
- [Comparison of alternatives](#comparison-of-alternatives)
1815
- [DeepCopy support](#deepcopy-support)
1916
- [Code Generator Changes](#code-generator-changes)
2017
- [Addition of applyconfiguration-gen](#addition-of-applyconfiguration-gen)
2118
- [client-gen changes](#client-gen-changes)
19+
- [read/modify/write loop support](#readmodifywrite-loop-support)
2220
- [Interoperability with structured and unstructured types](#interoperability-with-structured-and-unstructured-types)
2321
- [Test Plan](#test-plan)
2422
- [Fuzz-based round-trip testing](#fuzz-based-round-trip-testing)
@@ -37,6 +35,7 @@
3735
- [Implementation History](#implementation-history)
3836
- [Drawbacks](#drawbacks)
3937
- [Alternatives](#alternatives)
38+
- [Alternative: Generated structs where all fields are pointers](#alternative-generated-structs-where-all-fields-are-pointers)
4039
- [Alternative: Use YAML directly](#alternative-use-yaml-directly)
4140
- [Alternative: Combine go structs with fieldset mask](#alternative-combine-go-structs-with-fieldset-mask)
4241
- [Alternative: Use varadic function based builders](#alternative-use-varadic-function-based-builders)
@@ -61,7 +60,7 @@ checklist items _must_ be updated for the enhancement to be released.
6160
Items marked with (R) are required *prior to targeting to a milestone / release*.
6261

6362
- [x] (R) Enhancement issue in release milestone, which links to KEP dir in [kubernetes/enhancements] (not the initial KEP PR)
64-
- [ ] (R) KEP approvers have approved the KEP status as `implementable`
63+
- [x] (R) KEP approvers have approved the KEP status as `implementable`
6564
- [x] (R) Design details are appropriately documented
6665
- [ ] (R) Test plan is in place, giving consideration to SIG Architecture and SIG Testing input
6766
- [x] (R) Graduation criteria is in place
@@ -221,111 +220,38 @@ While there are arguments to be made all of these fields are expected to cause
221220
problems in practice, there are some that clearly would, e.g. ContainerPort.
222221

223222
Because of this we cannot use the existing go structs to represent apply
224-
configurations.
223+
configurations. Instead, we will generated "builders".
225224

226-
<<[UNRESOLVED @jpbetz @jennybuckley ]>>
227-
Finalize which alternative to use based on developer feedback. See the
228-
[Alternatives](#alternatives) for a complete list, but are currently focusing on
229-
the two below alternatives. We are working with the Kubebuilder community to
230-
gather feedback on what developers prefer.
231-
<<[/UNRESOLVED]>>
232-
233-
#### Alternative 1: Generated structs where all fields are pointers
234-
235-
Example usage:
236-
237-
```go
238-
import (
239-
ptr "k8s.io/utils/pointer"
240-
)
241-
242-
&appsv1apply.Deployment{
243-
Name: ptr.StringPtr("nginx-deployment"),
244-
Spec: &appsv1apply.DeploymentSpec{
245-
Replicas: ptr.Int32Ptr(0),
246-
Template: &v1apply.PodTemplate{
247-
Spec: &v1apply.PodSpec{
248-
Containers: []v1.Containers{
249-
{
250-
Name: ptr.StringPtr("nginx"),
251-
Image: ptr.StringPtr("nginx:latest"),
252-
},
253-
}
254-
},
255-
},
256-
},
257-
}
258-
```
259-
260-
For this approach, developers need to use a library like
261-
https://github.com/kubernetes/utils/blob/master/pointer/pointer.go
262-
to inline primitive literals.
263-
264-
#### Alternative 2: Generated "builders"
265-
266-
Example usage:
225+
Example generated builder:
267226

268227
```go
269-
&appsv1apply.Deployment().
270-
ObjectMeta(&metav1apply.ObjectMeta().
271-
Name("nginx-deployment")).
272-
Spec(&appsv1apply.DeploymentSpec().
228+
appsv1apply.Deployment("ns", "nginx-deployment").
229+
Spec(appsv1apply.DeploymentSpec().
273230
Replicas(0).
274231
Template(
275-
&v1apply.PodTemplate().
276-
Spec(&v1apply.SetPodSpec().
277-
Containers(v1apply.ContainerList{
232+
v1apply.PodTemplate().
233+
Spec(v1apply.PodSpec().
234+
Containers(
278235
v1apply.Container().
279236
Name("nginx").
280-
Image("nginx:1.14.2")
237+
Image("nginx:1.14.2"),
281238
v1apply.Container().
282239
Name("sidecar").
283-
})
240+
)
284241
)
285242
)
286243
)
287244
)
288245
```
289246

290-
<<[UNRESOLVED @jpbetz ]>>
291-
Finalize how setter functions are named if we go with this alternative. Options we
292-
are considering are: (1) Just use the field name (2) prefix field name with "Set" (3)
293-
prefix field name with "With".
294-
<<[/UNRESOLVED]>>
295-
296-
#### Comparison of alternatives
297-
298-
See https://github.com/kubernetes/kubernetes/pull/95988 for a working implementation
299-
of alterative 1 and https://github.com/jpbetz/kubernetes/tree/apply-client-go-builders
300-
for a working implementation of alternative 2.
247+
See https://github.com/jpbetz/kubernetes/tree/apply-client-go-builders for a working
248+
implementation.
301249

302-
Of the two leading alternatives--"builders" and "structs with pointers"--we implemented
303-
prototypes of both. They had roughly equivalent performance, and no differences
304-
in their capabilities. The choice between the two is primarily one of aesthetics/ergonomics.
250+
The namespace and name will be immutable once set on the constructor. The constructor
251+
will be generated according to the scope of the object - cluster scoped objects will
252+
not have a namespace argument.
305253

306-
Some of the feedback we have heard from the community:
307-
308-
- "structs with pointers" feels more go idiomatic and more closely aligned with
309-
the go structs used for Kubernetes types both for builtin types and by
310-
Kubebuilder.
311-
- It's nice how "builders" are clearly visually distinct from the main go types.
312-
- Having to use a utility library to wrap literal values as pointers for the
313-
"structs with pointers" is not a big deal. I'm already familiar
314-
with having to do this in go and once I learn use a utility library for it
315-
I'm all set.
316-
- The "builders" are awkward to use in an IDE. I felt like I was fighting with
317-
my IDE to get chain function calls and organize them hierarchally as expected
318-
by this approach.
319-
320-
TODO: We are providing the developer community with a fork of controller-tools
321-
that will allow them to generate apply configuration types that have both the
322-
alternatives. Our goal is to get feedback from developers after they try out the
323-
generated apply configurations and use that to inform our decision.
324-
325-
While it is possible to generate types that have both the pointer fields exposed
326-
and the builder functions, we would prefer to provide clear guidance to the
327-
community on how apply configurations should be represented in go and encourage
328-
consistent use of only one of these approaches.
254+
TypeMeta info (apiVersion and type) is autopopulated by the constructor.
329255

330256
#### DeepCopy support
331257

@@ -358,6 +284,67 @@ adding functions to interfaces is a change we have made in the past, and develop
358284
that have alternate implementations of the interface will usually get a compiler
359285
error in this case, which is relatively trivial to resolve
360286

287+
### read/modify/write loop support
288+
289+
While it is
290+
[recommended](https://kubernetes.io/docs/reference/using-api/server-side-apply/)
291+
that apply clients use "fully specified intent" rather than
292+
read/modify/write loops, there are many existing controllers that use
293+
a get/modify/update loop today, and are not easy to convert to the
294+
"fully specified intend" approach.
295+
296+
For such controllers, providing a convenient way to do a
297+
"get-previously-applied/modify/apply" loop is pragmatic. It allows
298+
these controller to benefit from apply by sending a minimal apply
299+
patch (as opposed to sending the entire object for update) and to
300+
reduce the odds of conflict with other controllers.
301+
302+
To support this, the builders will include "BuildApply" utility functions
303+
function. For example:
304+
305+
```
306+
fieldManger := "my-field-manager"
307+
// 1. read
308+
deployment, err := client.AppsV1().Deployments(ns).Get("deployment-name", metav1.GetOptions{})
309+
if err != nil {
310+
// handle err
311+
}
312+
applyConfig, err := appsv1apply.BuildDeploymentApply(deployment, fieldManager)
313+
if err != nil {
314+
// handle err
315+
}
316+
317+
// 2. modify
318+
applyConfig.GetSpec().Replicas(10)
319+
320+
// 3. apply
321+
client.AppsV1().Deployments(ns).Apply(ctx, applyConfig, metav1.ApplyOptions{FieldManager: fieldManager, Force: true})
322+
```
323+
324+
In the above example, `BuildDeploymentApply` constructs a populated
325+
apply configuration from a deployment object returned from a Get. It
326+
uses the provided field manager to get the matching field set
327+
(`FieldsV1` data) from object and then combines the field set with the
328+
object to produce the apply configuration.
329+
330+
We will clearly document that on the "BuildApply" functions that we
331+
recommend using "fully specified intent" when using apply and also
332+
when it might be appropriate (and inappropriate) to use the "BuildApply"
333+
utility.
334+
335+
An alternative we considered, but rejected, was to add `GetForApply()`
336+
style functions to the client. The benefit of this approach is that
337+
the caller doesn't have to deal with converting the response object
338+
into an apply configuration, and there is no possibility of the client
339+
modifying the object before converting it into an apply
340+
configuration. The biggest problem with this approach is that there
341+
are many methods that return an object
342+
(get/create/update/patch/watch/list) that would all need to have a
343+
corresponding `<method>ForApply()`.
344+
345+
Corner case: If the version of the managed fields does not match the
346+
object (which is possible, but only if changing versions and using the
347+
same field manager) the "BuildApply" function will return an error.
361348

362349
### Interoperability with structured and unstructured types
363350

@@ -368,6 +355,11 @@ For "builders", each would implement `MarshalJSON`, `UnmarshalJSON`,
368355
`ToUnstructured` and `FromUnstructured`. Builders would also provide getter functions
369356
to view what has been built.
370357

358+
`FromUnstructured` will check for the presence managed fields in the unstructured data,
359+
if present, it will fail with an error indicating that objects retrieved via a Get
360+
cannot be converted using `FromUnstructured` and should instead be converted to an
361+
apply configuration using a "BuildApply" function.
362+
371363
### Test Plan
372364

373365
#### Fuzz-based round-trip testing
@@ -389,8 +381,11 @@ it is correct using the existing e2e tests, expanding coverage as needed.
389381

390382
### Graduation Criteria
391383

392-
This enhancement will graduate to GA as part of Server Side Apply. It does
393-
not make sense to graduate it independently.
384+
Because client-go has no feature gates, the gating of this
385+
functionality is determined by the Server Side Apply
386+
functionality. For this reason this enhancement is a considered a
387+
sub-KEP of Server Side Apply, and will graduate to GA as part of
388+
Server Side Apply, which is slated for 1.21.
394389

395390
### Upgrade / Downgrade Strategy
396391

@@ -500,6 +495,64 @@ Major milestones might include:
500495

501496
## Alternatives
502497

498+
#### Alternative: Generated structs where all fields are pointers
499+
500+
Example usage:
501+
502+
```go
503+
import (
504+
ptr "k8s.io/utils/pointer"
505+
)
506+
507+
&appsv1apply.Deployment{
508+
Name: ptr.StringPtr("nginx-deployment"),
509+
Spec: &appsv1apply.DeploymentSpec{
510+
Replicas: ptr.Int32Ptr(0),
511+
Template: &v1apply.PodTemplate{
512+
Spec: &v1apply.PodSpec{
513+
Containers: []v1.Containers{
514+
{
515+
Name: ptr.StringPtr("nginx"),
516+
Image: ptr.StringPtr("nginx:latest"),
517+
},
518+
}
519+
},
520+
},
521+
},
522+
}
523+
```
524+
525+
There was mixed support for this approach in the community. Some feedback we have
526+
gotten when comparing the "builders" alternative with this "structs with pointers"
527+
alternative include:
528+
529+
- "structs with pointers" feels more go idiomatic and more closely aligned with
530+
the go structs used for Kubernetes types both for builtin types and by
531+
Kubebuilder.
532+
- It's nice how "builders" are clearly visually distinct from the main go types.
533+
- Having to use a utility library to wrap literal values as pointers for the
534+
"structs with pointers" is not a big deal. I'm already familiar
535+
with having to do this in go and once I learn use a utility library for it
536+
I'm all set.
537+
- The "builders" are awkward to use in an IDE. I felt like I was fighting with
538+
my IDE to get chain function calls and organize them hierarchally as expected
539+
by this approach.
540+
541+
Limitations:
542+
543+
- Even when using a library like
544+
https://github.com/kubernetes/utils/blob/master/pointer/pointer.go to
545+
inline primitive literals, some values, like enumeration values cannot
546+
be inlined since ptr.StringPtr() does not work with them.
547+
- Direct access to struct allow developers to do conversions that are
548+
unsafe, like directly converting from a type's go struct, retrieved
549+
via a Get, to it apply configuration without using the managed
550+
fields during the conversion.
551+
- No good way to future proof this against adding tombstone support in the future
552+
like there is with the builders approach.
553+
- Code that reads the value of a deeply nested field because it must defererence
554+
pointers at each level of nesting.
555+
503556
### Alternative: Use YAML directly
504557

505558
For fields that need to be set programmatically, use templating.

keps/sig-api-machinery/2144-clientgo-apply/kep.yaml renamed to keps/sig-api-machinery/2155-clientgo-apply/kep.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
title: Client-go Apply
2-
kep-number: 2144
2+
kep-number: 2155
33
authors:
44
- "@jpbetz"
55
owning-sig: sig-api-machinery
6-
status: provisional
6+
status: implementable
77
creation-date: 2020-11-16
88
reviewers:
99
- "@apelisse"
@@ -17,18 +17,18 @@ see-also:
1717
- "/keps/sig-api-machinery/0006-apply.md"
1818

1919
# The target maturity stage in the current dev cycle for this KEP.
20-
# stage: alpha
20+
stage: stable
2121

2222
# The most recent milestone for which work toward delivery of this KEP has been
2323
# done. This can be the current (upcoming) milestone, if it is being actively
2424
# worked on.
25-
# latest-milestone: "v1.21"
25+
latest-milestone: "v1.21"
2626

2727
# The milestone at which this feature was, or is targeted to be, at each stage.
28-
# milestone:
29-
# alpha: "v1.21"
30-
# beta: "v1.21"
31-
# stable: "v1.21"
28+
milestone:
29+
alpha: "v1.21"
30+
beta: "v1.21"
31+
stable: "v1.21"
3232

3333
# The following PRR answers are required at alpha release
3434
# List the feature gate name and the components for which it must be enabled

0 commit comments

Comments
 (0)