Skip to content

Commit 5100aa8

Browse files
committed
Update unions KEP for v1.25
1 parent cd96e98 commit 5100aa8

File tree

1 file changed

+134
-111
lines changed
  • keps/sig-api-machinery/1027-api-unions

1 file changed

+134
-111
lines changed

keps/sig-api-machinery/1027-api-unions/README.md

Lines changed: 134 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,14 @@ become possible.
125125
### Goals
126126

127127
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.
129130

130131
### Non-Goals
131132

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)
134136

135137
## Proposal
136138

@@ -147,22 +149,40 @@ Detail the things that people will be able to do if this KEP is implemented.
147149
Include as much detail as possible so that people can understand the "how" of
148150
the system. The goal here is to make this feel real for users without getting
149151
bogged down.
152+
153+
TODO: ask antoine if these stories are complete/sufficient
150154
-->
151155

156+
152157
#### Story 1
153158

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+
154164
#### Story 2
155165

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+
156169
### Notes/Constraints/Caveats (Optional)
157170

158171
<!--
172+
TODO: ?
159173
What are the caveats to the proposal?
160174
What are some important details that didn't come across above?
161175
Go in to as much detail as necessary here.
162176
This might be a good place to talk about core concepts and how they relate.
163177
-->
164178

165179
### 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.
166186

167187
<!--
168188
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.
183203
We're proposing a new type of tags for go types (in-tree types, and also
184204
kubebuilder types):
185205

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)).
195206
- `// +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
201216
same structure), examples of what is allowed:
202217

203218
```
@@ -209,27 +224,28 @@ type TopLevelUnion struct {
209224
}
210225
211226
// This will generate one union, with two fields and a discriminator.
212-
// +union
213227
type Union struct {
214228
// +unionDiscriminator
215229
// +optional
216230
UnionType string `json:"unionType"`
217231
218232
// +optional
233+
// +unionMember
219234
FieldA int `json:"fieldA"`
220-
// +optional
235+
// +optional
221236
FieldB int `json:"fieldB"`
222237
}
223238
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
225240
type Union2 struct {
226241
// +unionDiscriminator
242+
// +required
227243
Type string `json:"type"`
228-
// +unionDeprecated
229-
// +optional
244+
// +unionMember=type
245+
// +optional
230246
Alpha int `json:"alpha"`
231-
// +unionDeprecated
232-
// +optional
247+
// +unionMember=type
248+
// +optional
233249
Beta int `json:"beta"`
234250
}
235251
@@ -238,14 +254,19 @@ type Union2 struct {
238254
type InlinedUnion struct {
239255
Name string `json:"name"`
240256
241-
// +unionDeprecated
257+
// +unionDiscriminator
258+
// +optional
259+
FieldType string `json:"fieldType"`
260+
// +unionMember=fieldType
242261
// +optional
243262
Field1 *int `json:"field1,omitempty"`
244-
// +unionDeprecated
263+
// +unionMember=fieldType
245264
// +optional
246265
Field2 *int `json:"field2,omitempty"`
247266
248-
Union `json:",inline"`
267+
// Union does not label its members, so it
268+
cannot be inlined
269+
union Union `json:"union"`
249270
Union2 `json:",inline"`
250271
}
251272
```
@@ -271,6 +292,7 @@ Conversion between OpenAPI v2 and OpenAPI v3 will preserve these fields.
271292

272293
### Discriminator
273294

295+
// TODO: ask Antoine
274296
For backward compatibility reasons, discriminators should be added to existing
275297
union structures as an optional string. This has a nice property that it's going
276298
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
287309
will be interpreted as an intention to clear all the other fields. See section
288310
below.
289311

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-
312312
#### "At most one" versus "exactly one"
313313

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).
317322

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
320324

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.
322328

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.
327332

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.
329336

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.
339343

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.
340349

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.
342355

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.
347357

348358
### Test Plan
349359

@@ -382,21 +392,46 @@ https://testgrid.k8s.io/sig-testing-canaries#ci-kubernetes-coverage-unit
382392
383393
This can inform certain test coverage improvements that we want to do before
384394
extending the production code to implement this enhancement.
385-
-->
386395
387396
- `<package>`: `<date>` - `<test coverage>`
388397
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+
389409
##### Integration tests
390410

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.
391426
<!--
392427
This question should be filled when targeting a release.
393428
For Alpha, describe what tests will be added to ensure proper quality of the enhancement.
394429
395430
For Beta and GA, add links to added tests together with links to k8s-triage for those tests:
396431
https://storage.googleapis.com/k8s-triage/index.html
397-
-->
398432
399433
- <test>: <link to test coverage>
434+
-->
400435

401436
##### e2e tests
402437

@@ -408,21 +443,21 @@ For Beta and GA, add links to added tests together with links to k8s-triage for
408443
https://storage.googleapis.com/k8s-triage/index.html
409444
410445
We expect no non-infra related flakes in the last month as a GA graduation criteria.
411-
-->
412446
413447
- <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.
414451

415452
### Graduation Criteria
416453

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
420455

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.
426461

427462
### Upgrade / Downgrade Strategy
428463

@@ -785,42 +820,30 @@ For each of them, fill in the following information by copying the below templat
785820

786821
###### What steps should be taken if SLOs are not being met to determine the problem?
787822

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-
801823
## 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.
813825

814826
## Drawbacks
815827

816828
<!--
817829
Why should this KEP _not_ be implemented?
818830
-->
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
821839

822840
## Alternatives
823841
* 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.
824847

825848
<!--
826849
What other approaches did you consider, and why did you rule them out? These do

0 commit comments

Comments
 (0)