@@ -125,12 +125,14 @@ become possible.
125
125
### Goals
126
126
127
127
The goal is to enable a union or "oneof" semantics in Kubernetes types, both for
128
- in-tree types and for CRDs.
128
+ in-tree types and for CRDs. Validation of these unions should be centralized
129
+ rather than being written on a per resource basis in validation functions.
129
130
130
131
### Non-Goals
131
132
132
- We're not planning to use this KEP to release the feature, but mostly as a way
133
- to document what we're doing.
133
+ A priority will be minimum disruption to existing types rather than ensuring all
134
+ existing types conform to a single union paradigm (i.e. unions without a
135
+ discriminator should continue to not need a discriminator)
134
136
135
137
## Proposal
136
138
@@ -147,22 +149,40 @@ Detail the things that people will be able to do if this KEP is implemented.
147
149
Include as much detail as possible so that people can understand the "how" of
148
150
the system. The goal here is to make this feel real for users without getting
149
151
bogged down.
152
+
153
+ TODO: ask antoine if these stories are complete/sufficient
150
154
-->
151
155
156
+
152
157
#### Story 1
153
158
159
+ As a CRD owner, I should be able to author my CRD such that the openapi schema
160
+ indicates which fields are members of this oneOf, so that when users crate
161
+ custom resources, the oneOf fields will be automatically validated, without me
162
+ having to write custom validation logic.
163
+
154
164
#### Story 2
155
165
166
+ As a maintainer of existing builtin APIs, I should be able to add new fields to
167
+ a union and have it behave as expected.
168
+
156
169
### Notes/Constraints/Caveats (Optional)
157
170
158
171
<!--
172
+ TODO: ?
159
173
What are the caveats to the proposal?
160
174
What are some important details that didn't come across above?
161
175
Go in to as much detail as necessary here.
162
176
This might be a good place to talk about core concepts and how they relate.
163
177
-->
164
178
165
179
### Risks and Mitigations
180
+ - We need to ensure we do not break existing union types. This can be done by
181
+ not forcing existing unions to conform to the newly proposed union semantics.
182
+ Integration testing with older types should give us the confidence to be sure
183
+ we have done so.
184
+ - There is a lot of risk for errors when there exists skew between clients and
185
+ server. In the section on normalization, we discuss mitigating these risks.
166
186
167
187
<!--
168
188
What are the risks of this proposal, and how do we mitigate? Think broadly.
@@ -183,21 +203,16 @@ Consider including folks who also work outside the SIG or subproject.
183
203
We're proposing a new type of tags for go types (in-tree types, and also
184
204
kubebuilder types):
185
205
186
- - ` // +union ` before a structure means that the structure is a union. All the
187
- fields must be optional (beside the discriminator) and will be included as
188
- members of the union. That structure CAN be embedded in another structure.
189
- - ` // +unionDeprecated ` before a field means that it is part of the
190
- union. Multiple fields can have this prefix. These fields MUST BE optional,
191
- omitempty and pointers. The field is named deprecated because we don't want
192
- people to embed their unions directly in structures, and only exist because of
193
- some existing core types (e.g. ` Value ` and ` ValueFrom ` in
194
- [ EnvVar] ( https://github.com/kubernetes/kubernetes/blob/3ebb8ddd8a21b/staging/src/k8s.io/api/core/v1/types.go#L1817-L1836 ) ).
195
206
- ` // +unionDiscriminator ` before a field means that this field is the
196
- discriminator for the union. Only one field per structure can have this
197
- prefix. This field HAS TO be a string, and CAN be optional.
198
-
199
- Multiple unions can exist per structure, but unions can't span across multiple
200
- go structures (all the fields that are part of a union has to be together in the
207
+ discriminator for the union. This field MUST be a string. Can be optional or
208
+ required. If required, the union must have exactly one member set, if
209
+ optional, the union may have at most one member set.
210
+ - ` // +unionMember=<discriminatorName> ` before a field means that this
211
+ field is a member of a union. The ` <discriminatorName> ` is optional if there
212
+ is only union in a struct and required if there are multiple unions per
213
+ struct.
214
+
215
+ Note unions can't span across multiple go structures (all the fields that are part of a union has to be together in the
201
216
same structure), examples of what is allowed:
202
217
203
218
```
@@ -209,27 +224,28 @@ type TopLevelUnion struct {
209
224
}
210
225
211
226
// This will generate one union, with two fields and a discriminator.
212
- // +union
213
227
type Union struct {
214
228
// +unionDiscriminator
215
229
// +optional
216
230
UnionType string `json:"unionType"`
217
231
218
232
// +optional
233
+ // +unionMember
219
234
FieldA int `json:"fieldA"`
220
- // +optional
235
+ // +optional
221
236
FieldB int `json:"fieldB"`
222
237
}
223
238
224
- // This also generates one union, with two fields and on discriminator.
239
+ // This generates two unions, each with two fields and a discriminator. The
225
240
type Union2 struct {
226
241
// +unionDiscriminator
242
+ // +required
227
243
Type string `json:"type"`
228
- // +unionDeprecated
229
- // +optional
244
+ // +unionMember=type
245
+ // +optional
230
246
Alpha int `json:"alpha"`
231
- // +unionDeprecated
232
- // +optional
247
+ // +unionMember=type
248
+ // +optional
233
249
Beta int `json:"beta"`
234
250
}
235
251
@@ -238,14 +254,19 @@ type Union2 struct {
238
254
type InlinedUnion struct {
239
255
Name string `json:"name"`
240
256
241
- // +unionDeprecated
257
+ // +unionDiscriminator
258
+ // +optional
259
+ FieldType string `json:"fieldType"`
260
+ // +unionMember=fieldType
242
261
// +optional
243
262
Field1 *int `json:"field1,omitempty"`
244
- // +unionDeprecated
263
+ // +unionMember=fieldType
245
264
// +optional
246
265
Field2 *int `json:"field2,omitempty"`
247
266
248
- Union `json:",inline"`
267
+ // Union does not label its members, so it
268
+ cannot be inlined
269
+ union Union `json:"union"`
249
270
Union2 `json:",inline"`
250
271
}
251
272
```
@@ -271,6 +292,7 @@ Conversion between OpenAPI v2 and OpenAPI v3 will preserve these fields.
271
292
272
293
### Discriminator
273
294
295
+ // TODO: ask Antoine
274
296
For backward compatibility reasons, discriminators should be added to existing
275
297
union structures as an optional string. This has a nice property that it's going
276
298
to allow conflict detection when the selected union field is changed.
@@ -287,63 +309,51 @@ When the value of the discriminator is explicitly changed by the client, it
287
309
will be interpreted as an intention to clear all the other fields. See section
288
310
below.
289
311
290
- ### Normalizing on updates
291
-
292
- A "normalization process" will run automatically for all creations and
293
- modifications (either with update or patch). It will happen automatically in order
294
- to clear fields and update discriminators. This process will run for both
295
- core-types and CRDs. It will take place before validation. The sent object
296
- doesn't have to be valid, but fields will be cleared in order to make it valid.
297
- This process will also happen before fields tracking (server-side apply), so
298
- changes in discriminator, even if implicit, will be owned by the client making
299
- the update (and may result in conflicts).
300
-
301
- This process works as follows:
302
- - If there is a discriminator, and its value has changed, clear all fields but
303
- the one specified by the discriminator,
304
- - If there is no discriminator, or if its value hasn't changed,
305
- - if there is exactly one field, set the discriminator when there is one
306
- to that value. Otherwise,
307
- - compare the fields set before and after. If there is exactly one field
308
- added, set the discriminator (if present) to that value, and remove all
309
- other fields. if more than one field has been added, leave the process so
310
- that validation will fail.
311
-
312
312
#### "At most one" versus "exactly one"
313
313
314
- The goal of this proposal is not to change the validation, but to help clients
315
- to clear other fields in the union. Validation should be implemented for in-tree
316
- types as it is today, or through "oneOf" properties in CRDs.
314
+ // TODO: check this
315
+ We propose supporting both "at most one" and "exactly one" semantics.
316
+
317
+ To distinguish between the two, API authors should tag the discriminator as
318
+ "optional" or "required" based on the desired behavior. We the discriminator is
319
+ set to an empty string (or missing from the object), this will indicate to the
320
+ server to clear all fields of the union (or fail the request if the
321
+ discriminator is required).
317
322
318
- In other word, this is proposing to implement "at most one", and the exactly one
319
- should be provided through another layer of validation (separated).
323
+ ### Normalization and Validation
320
324
321
- #### Clearing all the fields
325
+ Normalization refers to the process by which the API server attempts to understand
326
+ and correct clients which may provide the server with conflicting or incomplete
327
+ information about a union.
322
328
323
- Since the system is trying to do the right thing, it can be hard to "clear
324
- everything". In that case, each API could decide to have their own "Nothing"
325
- value in the discriminator, which will automatically trigger a clearing of all
326
- fields beside "Nothing".
329
+ Issues primarily arise here because of version skew between a client and server,
330
+ such as when a client is unaware of new fields added to a union and thus doesn't
331
+ know how to clear these new fields when trying to set a different.
327
332
328
- ### Backward compatibilities properties
333
+ For unions with a discriminator (all newly created unions and some existing
334
+ unions), normalization is simple: the server should always respect the
335
+ discriminator.
329
336
330
- This normalization process has a few nice properties, especially for dumb
331
- clients, when it comes to backward compatibility:
332
- - A dumb client that doesn't know which fields belong to the union can just set
333
- a new field and get all the others cleared automatically
334
- - A dumb client that doesn't know about the discriminator is going to change a
335
- field, leave the discriminator as it is, and should still expect the fields to
336
- be cleared accordingly
337
- - A dumb client that knows about the discriminator can change the discriminator
338
- without knowing which fields to clear, they will get cleared automatically
337
+ This means two things:
338
+ 1 . when the server receives a request with a discriminator set to a
339
+ given field, it should clear out any other fields that are set.
340
+ 2 . when the server receives a request with a discriminator set to a given field,
341
+ but that given field is empty, it should maintain the value of the field that
342
+ currently exists.
339
343
344
+ For union types without a discriminator (only existing unions), normalization is a little more
345
+ complicated and incomplete. If multiple union fields are set:
346
+ 1 . If greater than two of the set union fields were not previously set, error
347
+ 2 . If one of the set union fields was previously set, and one is newly set,
348
+ respect the newly set field and clear the other.
340
349
341
- ### Validation
350
+ For situations where a typed client is unaware of a new field (that is currently
351
+ set) and drops the set field such that no fields are now set, the server will
352
+ clear all the fields of the union. This is unavoidable for unions without a
353
+ discriminator and a major reason why we recommend all new unions have a
354
+ discriminator.
342
355
343
- Objects have to be validated AFTER the normalization process, which is going to
344
- leave multiple fields of the union if it can't normalize. As discussed in
345
- drawbacks below, it can also be useful to validate apply requests before
346
- applying them.
356
+ Objects must be validated AFTER the normalization process.
347
357
348
358
### Test Plan
349
359
@@ -382,21 +392,46 @@ https://testgrid.k8s.io/sig-testing-canaries#ci-kubernetes-coverage-unit
382
392
383
393
This can inform certain test coverage improvements that we want to do before
384
394
extending the production code to implement this enhancement.
385
- -->
386
395
387
396
- `<package>`: `<date>` - `<test coverage>`
388
397
398
+ -->
399
+ Core functionality will be extensively unit tested in the SMD typed package
400
+ (union_test.go).
401
+
402
+ Parts of the kubernetes endpoints handlers package that are modified to call
403
+ into the SMD code will also be unit tested as appropriate.
404
+
405
+
406
+
407
+
408
+
389
409
##### Integration tests
390
410
411
+ We will have extensive integration testing of the union code in the
412
+ ` test/integration/apiserver ` package.
413
+
414
+ We will be testing along the dimensions of:
415
+ * Which fields of the union get modified (none, existing fields, newly updated
416
+ fields)
417
+ * Type of union (discriminated vs non-discriminated)
418
+ * Whether the client is aware of all the fields
419
+ * Whether the server is aware of all fields
420
+ * Whether the union is optional or required
421
+
422
+ A fully documented test matrix exists in a [ google
423
+ spreadsheet] ( https://docs.google.com/spreadsheets/d/1dIOOqgrgvMI9b2412eVuRSydEaOxhYOoqk6bUjZOY9c/edit?resourcekey=0-wlOfJTC_EX-qpU680STHMA#gid=3601413 ) along with a
424
+ [ guide
425
+ doc] ( https://docs.google.com/document/d/1Wruosjo0ELLl1yxauzpsUjgH2fK9KdgXDmOdJ5sG7Kg/edit?resourcekey=0-8Pwzx6EvsFR7VQoXzCTY4Q ) on how to read and understand the test matrix.
391
426
<!--
392
427
This question should be filled when targeting a release.
393
428
For Alpha, describe what tests will be added to ensure proper quality of the enhancement.
394
429
395
430
For Beta and GA, add links to added tests together with links to k8s-triage for those tests:
396
431
https://storage.googleapis.com/k8s-triage/index.html
397
- -->
398
432
399
433
- <test>: <link to test coverage>
434
+ -->
400
435
401
436
##### e2e tests
402
437
@@ -408,21 +443,21 @@ For Beta and GA, add links to added tests together with links to k8s-triage for
408
443
https://storage.googleapis.com/k8s-triage/index.html
409
444
410
445
We expect no non-infra related flakes in the last month as a GA graduation criteria.
411
- -->
412
446
413
447
- <test>: <link to test coverage>
448
+ -->
449
+ We are considering adding kubectl e2e tests to mimic kubectl users performing
450
+ various operations on objects with union fields.
414
451
415
452
### Graduation Criteria
416
453
417
- Since this project is a sub-project of Server-side apply, it will be introduced
418
- directly as Beta, and will graduate to GA in a later release, according to the
419
- criteria below.
454
+ #### Alpha Graduation
420
455
421
- #### Beta -> GA Graduation
422
-
423
- - CRD support has been proven successful
424
- - Core-types all implement the semantics properly
425
- - Stable and bug-free for two releases
456
+ - CRDs can be created with union fields and are properly validated when
457
+ created/updated.
458
+ - Existing unions that have discriminators, are properly tagged and validated via
459
+ union validation
460
+ - Existing unions that don't have discriminators do not break when upgraded.
426
461
427
462
### Upgrade / Downgrade Strategy
428
463
@@ -785,42 +820,30 @@ For each of them, fill in the following information by copying the below templat
785
820
786
821
###### What steps should be taken if SLOs are not being met to determine the problem?
787
822
788
- ## Future Work
789
-
790
- Since the proposal only normalizes the object after the patch has been applied,
791
- it is hard to fail if the patch is invalid. There are scenarios where the patch
792
- is invalid but it results in an unpredictable object. For example, if a patch
793
- sends both a discriminator and a field that is not the discriminated field, it
794
- will either clear the value sent if the discriminator changes, or it will change
795
- the value of the sent discriminator.
796
-
797
- Validating patches is not a problem that we want to tackle now, but we can
798
- validate "Apply" objects to make sure that they do not define such broken
799
- semantics.
800
-
801
823
## Implementation History
802
-
803
- Here are the major milestones for this KEP:
804
- - Initial discussion happened a year before the creation of this kep:
805
- https://docs.google.com/document/d/1lrV-P25ZTWukixE9ZWyvchfFR0NE2eCHlObiCUgNQGQ/edit#heading=h.w5eqnf1f76x5
806
- - Points made in the initial document have been improved and put into this kep,
807
- which has approved by sig-api-machinery tech-leads
808
- - KEP has been implemented:
809
- - logic mostly lives in sigs.k8s.io/structured-merge-diff
810
- - conversion between schema and openapi definition are in k8s.io/kube-openapi
811
- - core types have been modified in k8s.io/kubernetes
812
- - Feature is ready to be released in Beta in kubernetes 1.15
824
+ - Unions implemented, but disabled in SMD.
813
825
814
826
## Drawbacks
815
827
816
828
<!--
817
829
Why should this KEP _not_ be implemented?
818
830
-->
819
- * Stutter with discriminator
820
- * Inconsistent for existing types here
831
+ An issue that one might have with requiring a discriminator is that it might
832
+ seem redundant to have to set a field _ and_ set another field indicating to the
833
+ server to use the set field. The reasons for doing so are discussed above in the
834
+ normalization section.
835
+
836
+ One other drawback is that our approach does not standardize all existing unions
837
+ into a single format. We don't see a way to do so without drastically changing
838
+ existing APIs and breaking backwards compatibility
821
839
822
840
## Alternatives
823
841
* Non-Discrminated
842
+ The primary alternative discussed is to not have a discriminator for new union
843
+ types. As discussed in the normalization section, requiring a discriminator
844
+ allows the server to better understand the intentions of clients that do not
845
+ have knowledge of all the fields in a union if newer verisons of the server add
846
+ new fields to the union.
824
847
825
848
<!--
826
849
What other approaches did you consider, and why did you rule them out? These do
0 commit comments