Skip to content

Commit e76861b

Browse files
committed
Address open questions based on meeting feedback
1 parent 7d09cfc commit e76861b

File tree

1 file changed

+100
-62
lines changed
  • keps/sig-api-machinery/1027-api-unions

1 file changed

+100
-62
lines changed

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

Lines changed: 100 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
- [Design Details](#design-details)
1818
- [Discriminator Field](#discriminator-field)
1919
- [Go Markers](#go-markers)
20+
- [Discriminator Values](#discriminator-values)
21+
- [Empty Union Members](#empty-union-members)
22+
- [Examples](#examples)
2023
- [OpenAPI](#openapi)
2124
- [Normalization and Validation](#normalization-and-validation)
2225
- [Migrating existing unions](#migrating-existing-unions)
23-
- [Open Questions](#open-questions)
2426
- [Test Plan](#test-plan)
2527
- [Prerequisite testing updates](#prerequisite-testing-updates)
2628
- [Unit tests](#unit-tests)
@@ -133,10 +135,6 @@ functions (e.g. `pkg/apis/<group>/validation/validation.go`).
133135
best match the intent of the client, despite the client potentially having
134136
incomplete information
135137

136-
Another goal is to migrate at least one existing union to the new semantics in
137-
order to verify that migration is possible. Ideally migrate one of each type of
138-
existing union (discriminated and non-discriminated).
139-
140138
### Non-Goals
141139

142140
Migrating all existing unions away from their bespoke validation
@@ -242,19 +240,76 @@ kubebuilder types):
242240

243241

244242
- `// +unionDiscriminator` before a field means that this field is the
245-
discriminator for the union. This field MUST be a string. This field MUST be
243+
discriminator for the union. This field MUST be an enum defined as a string (see section on
244+
discriminator values). This field MUST be
246245
required.
247-
- `// +unionOptional` before a discriminator field means that by setting the
248-
discriminator to an empty string, none of the union fields are persisted
249-
(meaning at most one member of the union must be set). If not present on the
250-
discriminator, exactly one of the union members must be set in order to be
251-
valid.
252246
- `// +unionMember=<memberName>,discriminatedBy=<discriminatorName>` before a field means that this
253-
field is a member of a union. The `<memberName>` is the name of the field that must be set as the discriminator value. It defaults to the json representation of the field name if not specified. The `<discriminatorName>` is optional if there
247+
field is a member of a union. The `<memberName>` is the name of the field that will be set as the discriminator value.
248+
It MUST correspond to one of the valid enum values of the discriminator's enum
249+
type. It defaults to the `CamelCase` representation of the field name if not specified. The `<discriminatorName>` is optional if there
254250
is only union in a struct and required if there are multiple unions per
255-
struct. It should be the go representation of the field tagged with
251+
struct. It should be the `CamelCase` representation of the field tagged with
256252
`unionDiscriminator`.
257253

254+
#### Discriminator Values
255+
256+
Here we present a description of how discriminators and their valid values
257+
should be defined.
258+
259+
As described above, the discriminator field must be a string and required.
260+
Because, there are only a few specific values that the discriminator can be, we
261+
propose that all discriminators should be defined as an enum, and should be
262+
tagged so via the enum go marker `// +enum`.
263+
264+
Required unions will have the number of valid discriminator values equal to the
265+
number of member fields (see exception below on empty union members). Optional
266+
unions will have the number of valid discriminator values equal to the number of
267+
member fields, plus one additional value for when "no member" is desired. By
268+
convention, this "no member" discriminator value should be the empty string.
269+
270+
We define optional unions as union where "at most" one member field of the union
271+
must be non-nil (as opposed to a required union, where "exactly" one member
272+
field of the union must be non-nil).
273+
274+
##### Empty Union Members
275+
276+
In some cases there are more discriminator values than there are member fields
277+
defined in the struct when that specific member requires no configuration. An
278+
example is the `DeploymentStrategy` where it has one member field `rollingUpdate`,
279+
but two valid discriminator values `RollingUpdate` and `Recreate`. By using an
280+
enum as the discriminator value we are able to define values beyond the member
281+
fields in order to accommodate this pattern.
282+
283+
#### Examples
284+
285+
Below is an example of how to define a union based on the above design
286+
287+
```
288+
// +enum
289+
type UnionType string
290+
291+
const (
292+
FieldA UnionType = "FieldA"
293+
FieldB = "FieldC"
294+
FieldC = "FieldC"
295+
FieldNone = ""
296+
)
297+
298+
type Union struct {
299+
// +unionDiscriminator
300+
// +required
301+
UnionType UnionType
302+
303+
// +unionMember
304+
// +optional
305+
FieldA int
306+
// +unionMember
307+
// +optional
308+
FieldB int
309+
}
310+
```
311+
312+
258313
Note unions can't span across multiple go structures (all the fields that are part of a union has to be together in the
259314
same structure), examples of what is allowed:
260315

@@ -392,53 +447,6 @@ initial migration.
392447
For non-discriminated unions, there are a few relatively straightforward types
393448
that make good candidates for initial migration, such as `ContainerState`
394449

395-
### Open Questions
396-
397-
A few open questions exist with the design that need to be resolved with SIG API
398-
Machinery before moving forward.
399-
400-
1. What do we do when the discriminator is set (to the previously set value),
401-
but all union member fields are null? This likely implies that the client cleared
402-
the union member unintentionally, given that the discriminator value remains.
403-
Should the server:
404-
405-
a. Error loudly and inform the client that the field pointed to by the
406-
discriminator is null?
407-
408-
b. Retain the previously set member field, present on the old object, absent
409-
from the new object, but pointed to by the discriminator?
410-
2. How should a server behave when it receives a union with multiple fields set
411-
and the discriminator modified to point to the newly set member field?
412-
413-
a. reject the request,
414-
415-
b. accept the request but don't clear any fields
416-
417-
c. accept the request and clear all the fields except the one chosen by the
418-
discriminator
419-
3. What should the value of the discriminator be when no field in the union is
420-
to be set (i.e for optional unions).
421-
422-
a. mandate that the discriminator is the empty string for all optional unions?
423-
424-
b. make the field specified on a per union basis (defined in the go markers)?
425-
426-
c. mandate that the discriminator be unset (thus make the discriminator an
427-
optional field for optional unions)?
428-
4. What should we default the discriminator value of a member field to if not
429-
specified in the go markers?
430-
431-
a. the json representation of the field name
432-
433-
b. the go representation of the field name
434-
Should we even give users the ability to define it or just stick to whatever
435-
we decide the default is?
436-
5. Given that we want to migrate at least one existing union to use the new
437-
semantics, which union should we do? Must we upgrade both a discriminated and
438-
non-discriminated union for alpha or is only upgrading an existing
439-
discriminated union sufficient? Thoughts on my proposed types of
440-
`MetricStatus` (discriminated) and `ContainerState` (non-discriminated).
441-
442450
### Test Plan
443451

444452
[x] I/we understand the owners of the involved components may require updates to
@@ -502,6 +510,12 @@ A fully documented test matrix exists in a [google
502510
spreadsheet](https://docs.google.com/spreadsheets/d/1dIOOqgrgvMI9b2412eVuRSydEaOxhYOoqk6bUjZOY9c/edit?resourcekey=0-wlOfJTC_EX-qpU680STHMA#gid=3601413) along with a
503511
[guide
504512
doc](https://docs.google.com/document/d/1Wruosjo0ELLl1yxauzpsUjgH2fK9KdgXDmOdJ5sG7Kg/edit?resourcekey=0-8Pwzx6EvsFR7VQoXzCTY4Q) on how to read and understand the test matrix.
513+
514+
As part of implementing the test matrix we will be able to prove the viability
515+
of upgrading existing unions by writing tests to mimic using the standardized
516+
union semantics on existing unions (even if actually upgrading these unions is
517+
outside the scope of alpha graduation)
518+
505519
<!--
506520
This question should be filled when targeting a release.
507521
For Alpha, describe what tests will be added to ensure proper quality of the enhancement.
@@ -534,8 +548,8 @@ We expect no non-infra related flakes in the last month as a GA graduation crite
534548

535549
- CRDs can be created with union fields and are properly validated when
536550
created/updated.
537-
- At least one existing unions that has discriminators, is properly tagged and validated via
538-
union validation
551+
- Prove the viability of upgrading existing unions to the new semantics by
552+
mimicking existing unions in e2e tests.
539553
- Existing unions that don't have discriminators do not break when upgraded.
540554

541555
### Upgrade / Downgrade Strategy
@@ -991,6 +1005,30 @@ allows the server to better understand the intentions of clients that do not
9911005
have knowledge of all the fields in a union if newer versions of the server add
9921006
new fields to the union.
9931007

1008+
###### "None" Discriminator Values
1009+
1010+
A number of strategies were discussed around how to represent the "none" value
1011+
of the discriminator (see "Discriminator Values" section above).
1012+
1013+
* One alternative was to mandate the "none" value always be the empty string.
1014+
The advantage to this is its simplicity and not creating a situation where
1015+
different API authors define there "none" value differently, so that anyone
1016+
could immediately know that a discriminator set to "" (empty string), is not
1017+
selecting any of the member fields. Also, it would allow us to not have to
1018+
define the set of enum values for each discriminator (as we could just use the
1019+
name of the member field). The disadvantage is that by not defining the set of
1020+
enum values, we make it impossible to support the "empty union members" case.
1021+
* Another alternative was to make the discriminator a pointer to a string and
1022+
its value nil. The disadvantage here is that this requires more complicated
1023+
union validation logic (first do a nil check, then check the value) and makes
1024+
it harder to determine client intent on patches where the discriminator is not
1025+
set.
1026+
* A third alternative is to require all unions be defined in their own separate
1027+
struct. This was rejected because there are many existing unions that define
1028+
random fields that are not members in the union within the same struct as
1029+
fields that do make up the union and we hope to be able to migrate at least
1030+
some of the existing unions to the new semantics.
1031+
9941032
<!--
9951033
What other approaches did you consider, and why did you rule them out? These do
9961034
not need to be as detailed as the proposal, but should include enough

0 commit comments

Comments
 (0)