-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Document pluralization of API fields #2838
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -95,9 +95,11 @@ backward-compatibly. | |
|
||
Before talking about how to make API changes, it is worthwhile to clarify what | ||
we mean by API compatibility. Kubernetes considers forwards and backwards | ||
compatibility of its APIs a top priority. | ||
compatibility of its APIs a top priority. Compatibility is *hard*, especially | ||
handling issues around rollback-safety. This is something every API change | ||
must consider. | ||
|
||
An API change is considered forward and backward-compatible if it: | ||
An API change is considered compatible if it: | ||
|
||
* adds new functionality that is not required for correct behavior (e.g., | ||
does not add a new required field) | ||
|
@@ -107,24 +109,35 @@ does not add a new required field) | |
* which fields are required and which are not | ||
* mutable fields do not become immutable | ||
* valid values do not become invalid | ||
* explicitly invalid values do not become valid | ||
|
||
Put another way: | ||
|
||
1. Any API call (e.g. a structure POSTed to a REST endpoint) that worked before | ||
your change must work the same after your change. | ||
2. Any API call that uses your change must not cause problems (e.g. crash or | ||
degrade behavior) when issued against servers that do not include your change. | ||
3. It must be possible to round-trip your change (convert to different API | ||
1. Any API call (e.g. a structure POSTed to a REST endpoint) that succeeded | ||
before your change must succeed after your change. | ||
2. Any API call that does not use your change must behave the same as it did | ||
before your change. | ||
3. Any API call that uses your change must not cause problems (e.g. crash or | ||
degrade behavior) when issued against an API servers that do not include your | ||
change. | ||
4. It must be possible to round-trip your change (convert to different API | ||
versions and back) with no loss of information. | ||
4. Existing clients need not be aware of your change in order for them to | ||
continue to function as they did previously, even when your change is utilized. | ||
5. Existing clients need not be aware of your change in order for them to | ||
continue to function as they did previously, even when your change is in use. | ||
6. It must be possible to rollback to a previous version of API server that | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How many versions are required to be roll-backable? I think just 1, but we should say that? |
||
does not include your change and have no impact on API objects which do not use | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...and what of objects that are using your change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd add words, but there's not much to say - a feature got rolled back. There's always the possibility of weird behavior in those cases. What would you say here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we owe it to people to give better guidance about what kinds of impact are acceptable. But we can fix that later, I think we don't actually have a great answer at the moment.. |
||
your change. API objects that use your change will be impacted in case of a | ||
rollback. | ||
|
||
If your change does not meet these criteria, it is not considered strictly | ||
compatible, and may break older clients, or result in newer clients causing | ||
undefined behavior. | ||
If your change does not meet these criteria, it is not considered compatible, | ||
and may break older clients, or result in newer clients causing undefined | ||
behavior. Such changes are generally disallowed, though exceptions have been | ||
made in extreme cases (e.g. security or obvious bugs). | ||
|
||
Let's consider some examples. In a hypothetical API (assume we're at version | ||
v6), the `Frobber` struct looks something like this: | ||
Let's consider some examples. | ||
|
||
In a hypothetical API (assume we're at version v6), the `Frobber` struct looks | ||
something like this: | ||
|
||
```go | ||
// API v6. | ||
|
@@ -134,7 +147,7 @@ type Frobber struct { | |
} | ||
``` | ||
|
||
You want to add a new `Width` field. It is generally safe to add new fields | ||
You want to add a new `Width` field. It is generally allowed to add new fields | ||
without changing the API version, so you can simply change it to: | ||
|
||
```go | ||
|
@@ -146,29 +159,55 @@ type Frobber struct { | |
} | ||
``` | ||
|
||
The onus is on you to define a sane default value for `Width` such that rule #1 | ||
above is true - API calls and stored objects that used to work must continue to | ||
work. | ||
The onus is on you to define a sane default value for `Width` such that rules | ||
#1 and #2 above are true - API calls and stored objects that used to work must | ||
continue to work. | ||
|
||
For your next change you want to allow multiple `Param` values. You can not | ||
simply change `Param string` to `Params []string` (without creating a whole new | ||
API version) - that fails rules #1 and #2. You can instead do something like: | ||
simply remove `Param string` and add `Params []string` (without creating a | ||
whole new API version) - that fails rules #1, #2, #3, and #6. Nor can you | ||
simply add `Params []string` and use it instead - that fails #2 and #6. | ||
|
||
You must instead define a new field and the relationship between that field and | ||
the existing field(s). Start by adding the new plural field: | ||
|
||
```go | ||
// Still API v6, but kind of clumsy. | ||
// Still API v6. | ||
type Frobber struct { | ||
Height int `json:"height"` | ||
Width int `json:"width"` | ||
Param string `json:"param"` // the first param | ||
ExtraParams []string `json:"extraParams"` // additional params | ||
Params []string `json:"params"` // all of the params | ||
} | ||
``` | ||
|
||
Now you can satisfy the rules: API calls that provide the old style `Param` | ||
will still work, while servers that don't understand `ExtraParams` can ignore | ||
it. This is somewhat unsatisfying as an API, but it is strictly compatible. | ||
|
||
Part of the reason for versioning APIs and for using internal structs that are | ||
This new field must be inclusive of the singular field. In order to satisfy | ||
the compatibility rules you must handle all the cases of version skew, multiple | ||
clients, and rollbacks. This can be handled by defaulting or admission control | ||
logic linking the fields together with context from the API operation to get as | ||
close as possible to the user's intentions. | ||
|
||
Upon any mutating API operation: | ||
* If only the singular field is specified (e.g. an older client), API logic | ||
must populate plural[0] from the singular value, and de-dup the plural | ||
field. | ||
* If only the plural field is specified (e.g. a newer client), API logic must | ||
populate the singular value from plural[0]. | ||
* If both the singular and plural fields are specified, API logic must | ||
validate that the singular value matches plural[0]. | ||
* Any other case is an error and must be rejected. | ||
|
||
For this purpose "is specified" means the following: | ||
* On a create or patch operation: the field is present in the user-provided input | ||
* On an update operation: the field is present and has changed from the | ||
current value | ||
|
||
Older clients that only know the singular field will continue to succeed and | ||
produce the same results as before the change. Newer clients can use your | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably worth noting that old clients break when trying to update the singular value of plural values are specified (because 0 has changed), since a lot people may not realize the implications There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually you have it below, disregard |
||
change without impacting older clients. The API server can be rolled back and | ||
only objects that use your change will be impacted. | ||
|
||
Part of the reason for versioning APIs and for using internal types that are | ||
distinct from any one version is to handle growth like this. The internal | ||
representation can be implemented as: | ||
|
||
|
@@ -181,24 +220,26 @@ type Frobber struct { | |
} | ||
``` | ||
|
||
The code that converts to/from versioned APIs can decode this into the somewhat | ||
uglier (but compatible!) structures. Eventually, a new API version, let's call | ||
it v7beta1, will be forked and it can use the clean internal structure. | ||
The code that converts to/from versioned APIs can decode this into the | ||
compatible structure. Eventually, a new API version, e.g. v7beta1, | ||
will be forked and it can drop the singular field entirely. | ||
|
||
We've seen how to satisfy rules #1 and #2. Rule #3 means that you can not | ||
We've seen how to satisfy rules #1, #2, and #3. Rule #4 means that you can not | ||
extend one versioned API without also extending the others. For example, an | ||
API call might POST an object in API v7beta1 format, which uses the cleaner | ||
`Params` field, but the API server might store that object in trusty old v6 | ||
form (since v7beta1 is "beta"). When the user reads the object back in the | ||
v7beta1 API it would be unacceptable to have lost all but `Params[0]`. This | ||
means that, even though it is ugly, a compatible change must be made to the v6 | ||
API. | ||
API, as above. | ||
|
||
However, this is very challenging to do correctly. It often requires multiple | ||
For some changes, this can be challenging to do correctly. It may require multiple | ||
representations of the same information in the same API resource, which need to | ||
be kept in sync in the event that either is changed. For example, let's say you | ||
decide to rename a field within the same API version. In this case, you add | ||
units to `height` and `width`. You implement this by adding duplicate fields: | ||
be kept in sync should either be changed. | ||
|
||
For example, let's say you decide to rename a field within the same API | ||
version. In this case, you add units to `height` and `width`. You implement | ||
this by adding new fields: | ||
|
||
```go | ||
type Frobber struct { | ||
|
@@ -211,17 +252,17 @@ type Frobber struct { | |
|
||
You convert all of the fields to pointers in order to distinguish between unset | ||
and set to 0, and then set each corresponding field from the other in the | ||
defaulting pass (e.g., `heightInInches` from `height`, and vice versa), which | ||
runs just prior to conversion. That works fine when the user creates a resource | ||
from a hand-written configuration -- clients can write either field and read | ||
either field, but what about creation or update from the output of GET, or | ||
update via PATCH (see | ||
[In-place updates](https://kubernetes.io/docs/user-guide/managing-deployments/#in-place-updates-of-resources))? | ||
In this case, the two fields will conflict, because only one field would be | ||
updated in the case of an old client that was only aware of the old field (e.g., | ||
`height`). | ||
|
||
Say the client creates: | ||
defaulting logic (e.g. `heightInInches` from `height`, and vice versa). That | ||
works fine when the user creates a sends a hand-written configuration -- | ||
clients can write either field and read either field. | ||
|
||
But what about creation or update from the output of a GET, or update via PATCH | ||
(see [In-place updates](https://kubernetes.io/docs/user-guide/managing-deployments/#in-place-updates-of-resources))? | ||
In these cases, the two fields will conflict, because only one field would be | ||
updated in the case of an old client that was only aware of the old field | ||
(e.g. `height`). | ||
|
||
Suppose the client creates: | ||
|
||
```json | ||
{ | ||
|
@@ -252,17 +293,16 @@ then PUTs back: | |
} | ||
``` | ||
|
||
The update should not fail, because it would have worked before `heightInInches` | ||
was added. | ||
As per the compatibility rules, the update must not fail, because it would have | ||
worked before the change. | ||
|
||
## Backward compatibility gotchas | ||
|
||
* A single feature/property cannot be represented using multiple spec fields in the same API version | ||
simultaneously, as the example above shows. Only one field can be populated in any resource at a time, and the client | ||
needs to be able to specify which field they expect to use (typically via API version), | ||
on both mutation and read. Old clients must continue to function properly while only manipulating | ||
the old field. New clients must be able to function properly while only manipulating the new | ||
field. | ||
* A single feature/property cannot be represented using multiple spec fields | ||
simultaneously within an API version. Only one representation can be | ||
populated at a time, and the client needs to be able to specify which field | ||
they expect to use (typically via API version), on both mutation and read. As | ||
above, older clients must continue to function properly. | ||
|
||
* A new representation, even in a new API version, that is more expressive than an | ||
old one breaks backward compatibility, since clients that only understood the | ||
|
@@ -283,15 +323,15 @@ was added. | |
be set, it is acceptable to add a new option to the union if the [appropriate | ||
conventions](api-conventions.md#objects) were followed in the original object. | ||
Removing an option requires following the [deprecation process](https://kubernetes.io/docs/reference/deprecation-policy/). | ||
|
||
* Changing any validation rules always has the potential of breaking some client, since it changes the | ||
assumptions about part of the API, similar to adding new enum values. Validation rules on spec fields can | ||
neither be relaxed nor strengthened. Strengthening cannot be permitted because any requests that previously | ||
worked must continue to work. Weakening validation has the potential to break other consumers and generators | ||
of the API resource. Status fields whose writers are under our control (e.g., written by non-pluggable | ||
controllers), may potentially tighten validation, since that would cause a subset of previously valid | ||
values to be observable by clients. | ||
|
||
* Do not add a new API version of an existing resource and make it the preferred version in the same | ||
release, and do not make it the storage version. The latter is necessary so that a rollback of the | ||
apiserver doesn't render resources in etcd undecodable after rollback. | ||
|
@@ -308,16 +348,15 @@ was added. | |
|
||
## Incompatible API changes | ||
|
||
There are times when this might be OK, but mostly we want changes that meet this | ||
definition. If you think you need to break compatibility, you should talk to the | ||
Kubernetes team first. | ||
There are times when incompatible changes might be OK, but mostly we want | ||
changes that meet the above definitions. If you think you need to break | ||
compatibility, you should talk to the Kubernetes API reviewers first. | ||
|
||
Breaking compatibility of a beta or stable API version, such as v1, is | ||
unacceptable. Compatibility for experimental or alpha APIs is not strictly | ||
required, but breaking compatibility should not be done lightly, as it disrupts | ||
all users of the feature. Experimental APIs may be removed. Alpha and beta API | ||
versions may be deprecated and eventually removed wholesale, as described in the | ||
[versioning document](../design-proposals/release/versioning.md). | ||
all users of the feature. Alpha and beta API versions may be deprecated and | ||
eventually removed wholesale, as described in the [deprecation policy](https://kubernetes.io/docs/reference/deprecation-policy/). | ||
|
||
If your change is going to be backward incompatible or might be a breaking | ||
change for API consumers, please send an announcement to | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be confusing for a novice - how does valid values in previous bullet contrast to explicitly invalid, etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/explicitly/previously/ (if you meant something else, more explanation is needed)
Note that one might expect this and the prior line to have a symmetry over upgrade/downgrade, but they don't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I wanted to convey was the difference between "we expanded the regex to include dashes" from the case of open-ended enum fields, which is discussed elsewhere in the doc.
Suggestions?