Skip to content

Commit e26bb61

Browse files
committed
Document pluralization of API fields
Based on https://docs.google.com/document/d/1Z8Vbo7RmV_Wvs4k8mluHQC2_Zs8cEwMJmHwWBf9BcaA/edit and email discussions. Resist the urge to clean up the whole doc - that will have to come later.
1 parent f7f33f5 commit e26bb61

File tree

1 file changed

+102
-63
lines changed

1 file changed

+102
-63
lines changed

contributors/devel/api_changes.md

Lines changed: 102 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,11 @@ backward-compatibly.
9595

9696
Before talking about how to make API changes, it is worthwhile to clarify what
9797
we mean by API compatibility. Kubernetes considers forwards and backwards
98-
compatibility of its APIs a top priority.
98+
compatibility of its APIs a top priority. Compatibility is *hard*, especially
99+
handling issues around rollback-safety. This is something every API change
100+
must consider.
99101

100-
An API change is considered forward and backward-compatible if it:
102+
An API change is considered compatible if it:
101103

102104
* adds new functionality that is not required for correct behavior (e.g.,
103105
does not add a new required field)
@@ -107,24 +109,35 @@ does not add a new required field)
107109
* which fields are required and which are not
108110
* mutable fields do not become immutable
109111
* valid values do not become invalid
112+
* explicitly invalid values do not become valid
110113

111114
Put another way:
112115

113-
1. Any API call (e.g. a structure POSTed to a REST endpoint) that worked before
114-
your change must work the same after your change.
115-
2. Any API call that uses your change must not cause problems (e.g. crash or
116-
degrade behavior) when issued against servers that do not include your change.
117-
3. It must be possible to round-trip your change (convert to different API
116+
1. Any API call (e.g. a structure POSTed to a REST endpoint) that succeeded
117+
before your change must succeed after your change.
118+
2. Any API call that does not use your change must behave the same as it did
119+
before your change.
120+
3. Any API call that uses your change must not cause problems (e.g. crash or
121+
degrade behavior) when issued against an API servers that do not include your
122+
change.
123+
4. It must be possible to round-trip your change (convert to different API
118124
versions and back) with no loss of information.
119-
4. Existing clients need not be aware of your change in order for them to
120-
continue to function as they did previously, even when your change is utilized.
125+
5. Existing clients need not be aware of your change in order for them to
126+
continue to function as they did previously, even when your change is in use.
127+
6. It must be possible to rollback to a previous version of API server that
128+
does not include your change and have no impact on API objects which do not use
129+
your change. API objects that use your change will be impacted in case of a
130+
rollback.
121131

122-
If your change does not meet these criteria, it is not considered strictly
123-
compatible, and may break older clients, or result in newer clients causing
124-
undefined behavior.
132+
If your change does not meet these criteria, it is not considered compatible,
133+
and may break older clients, or result in newer clients causing undefined
134+
behavior. Such changes are generally disallowed, though exceptions have been
135+
made in extreme cases (e.g. security or obvious bugs).
125136

126-
Let's consider some examples. In a hypothetical API (assume we're at version
127-
v6), the `Frobber` struct looks something like this:
137+
Let's consider some examples.
138+
139+
In a hypothetical API (assume we're at version v6), the `Frobber` struct looks
140+
something like this:
128141

129142
```go
130143
// API v6.
@@ -134,7 +147,7 @@ type Frobber struct {
134147
}
135148
```
136149

137-
You want to add a new `Width` field. It is generally safe to add new fields
150+
You want to add a new `Width` field. It is generally allowed to add new fields
138151
without changing the API version, so you can simply change it to:
139152

140153
```go
@@ -146,29 +159,55 @@ type Frobber struct {
146159
}
147160
```
148161

149-
The onus is on you to define a sane default value for `Width` such that rule #1
150-
above is true - API calls and stored objects that used to work must continue to
151-
work.
162+
The onus is on you to define a sane default value for `Width` such that rules
163+
#1 and #2 above are true - API calls and stored objects that used to work must
164+
continue to work.
152165

153166
For your next change you want to allow multiple `Param` values. You can not
154-
simply change `Param string` to `Params []string` (without creating a whole new
155-
API version) - that fails rules #1 and #2. You can instead do something like:
167+
simply remove `Param string` and add `Params []string` (without creating a
168+
whole new API version) - that fails rules #1, #2, #3, and #6. Nor can you
169+
simply add `Params []string` and use it instead - that fails #2 and #6.
170+
171+
You must instead define a new field and the relationship between that field and
172+
the existing field(s). Start by adding the new plural field:
156173

157174
```go
158-
// Still API v6, but kind of clumsy.
175+
// Still API v6.
159176
type Frobber struct {
160177
Height int `json:"height"`
161178
Width int `json:"width"`
162179
Param string `json:"param"` // the first param
163-
ExtraParams []string `json:"extraParams"` // additional params
180+
Params []string `json:"params"` // all of the params
164181
}
165182
```
166183

167-
Now you can satisfy the rules: API calls that provide the old style `Param`
168-
will still work, while servers that don't understand `ExtraParams` can ignore
169-
it. This is somewhat unsatisfying as an API, but it is strictly compatible.
170-
171-
Part of the reason for versioning APIs and for using internal structs that are
184+
This new field must be inclusive of the singular field. In order to satisfy
185+
the compatibility rules you must handle all the cases of version skew, multiple
186+
clients, and rollbacks. This can be handled by defaulting or admission control
187+
logic linking the fields together with context from the API operation to get as
188+
close as possible to the user's intentions.
189+
190+
Upon any mutating API operation:
191+
* If only the singular field is specified (e.g. an older client), API logic
192+
must populate plural[0] from the singular value, and de-dup the plural
193+
field.
194+
* If only the plural field is specified (e.g. a newer client), API logic must
195+
populate the singular value from plural[0].
196+
* If both the singular and plural fields are specified, API logic must
197+
validate that the singular value matches plural[0].
198+
* Any other case is an error and must be rejected.
199+
200+
For this purpose "is specified" means the following:
201+
* On a create or patch operation: the field is present in the user-provided input
202+
* On an update operation: the field is present and has changed from the
203+
current value
204+
205+
Older clients that only know the singular field will continue to succeed and
206+
produce the same results as before the change. Newer clients can use your
207+
change without impacting older clients. The API server can be rolled back and
208+
only objects that use your change will be impacted.
209+
210+
Part of the reason for versioning APIs and for using internal types that are
172211
distinct from any one version is to handle growth like this. The internal
173212
representation can be implemented as:
174213

@@ -181,24 +220,26 @@ type Frobber struct {
181220
}
182221
```
183222

184-
The code that converts to/from versioned APIs can decode this into the somewhat
185-
uglier (but compatible!) structures. Eventually, a new API version, let's call
186-
it v7beta1, will be forked and it can use the clean internal structure.
223+
The code that converts to/from versioned APIs can decode this into the
224+
compatible structure. Eventually, a new API version, e.g. v7beta1,
225+
will be forked and it can drop the singular field entirely.
187226

188-
We've seen how to satisfy rules #1 and #2. Rule #3 means that you can not
227+
We've seen how to satisfy rules #1, #2, and #3. Rule #4 means that you can not
189228
extend one versioned API without also extending the others. For example, an
190229
API call might POST an object in API v7beta1 format, which uses the cleaner
191230
`Params` field, but the API server might store that object in trusty old v6
192231
form (since v7beta1 is "beta"). When the user reads the object back in the
193232
v7beta1 API it would be unacceptable to have lost all but `Params[0]`. This
194233
means that, even though it is ugly, a compatible change must be made to the v6
195-
API.
234+
API, as above.
196235

197-
However, this is very challenging to do correctly. It often requires multiple
236+
For some changes, this can be challenging to do correctly. It may require multiple
198237
representations of the same information in the same API resource, which need to
199-
be kept in sync in the event that either is changed. For example, let's say you
200-
decide to rename a field within the same API version. In this case, you add
201-
units to `height` and `width`. You implement this by adding duplicate fields:
238+
be kept in sync should either be changed.
239+
240+
For example, let's say you decide to rename a field within the same API
241+
version. In this case, you add units to `height` and `width`. You implement
242+
this by adding new fields:
202243

203244
```go
204245
type Frobber struct {
@@ -211,17 +252,17 @@ type Frobber struct {
211252

212253
You convert all of the fields to pointers in order to distinguish between unset
213254
and set to 0, and then set each corresponding field from the other in the
214-
defaulting pass (e.g., `heightInInches` from `height`, and vice versa), which
215-
runs just prior to conversion. That works fine when the user creates a resource
216-
from a hand-written configuration -- clients can write either field and read
217-
either field, but what about creation or update from the output of GET, or
218-
update via PATCH (see
219-
[In-place updates](https://kubernetes.io/docs/user-guide/managing-deployments/#in-place-updates-of-resources))?
220-
In this case, the two fields will conflict, because only one field would be
221-
updated in the case of an old client that was only aware of the old field (e.g.,
222-
`height`).
223-
224-
Say the client creates:
255+
defaulting logic (e.g. `heightInInches` from `height`, and vice versa). That
256+
works fine when the user creates a sends a hand-written configuration --
257+
clients can write either field and read either field.
258+
259+
But what about creation or update from the output of a GET, or update via PATCH
260+
(see [In-place updates](https://kubernetes.io/docs/user-guide/managing-deployments/#in-place-updates-of-resources))?
261+
In these cases, the two fields will conflict, because only one field would be
262+
updated in the case of an old client that was only aware of the old field
263+
(e.g. `height`).
264+
265+
Suppose the client creates:
225266

226267
```json
227268
{
@@ -252,17 +293,16 @@ then PUTs back:
252293
}
253294
```
254295

255-
The update should not fail, because it would have worked before `heightInInches`
256-
was added.
296+
As per the compatibility rules, the update must not fail, because it would have
297+
worked before the change.
257298

258299
## Backward compatibility gotchas
259300

260-
* A single feature/property cannot be represented using multiple spec fields in the same API version
261-
simultaneously, as the example above shows. Only one field can be populated in any resource at a time, and the client
262-
needs to be able to specify which field they expect to use (typically via API version),
263-
on both mutation and read. Old clients must continue to function properly while only manipulating
264-
the old field. New clients must be able to function properly while only manipulating the new
265-
field.
301+
* A single feature/property cannot be represented using multiple spec fields
302+
simultaneously within an API version. Only one representation can be
303+
populated at a time, and the client needs to be able to specify which field
304+
they expect to use (typically via API version), on both mutation and read. As
305+
above, older clients must continue to function properly.
266306

267307
* A new representation, even in a new API version, that is more expressive than an
268308
old one breaks backward compatibility, since clients that only understood the
@@ -283,15 +323,15 @@ was added.
283323
be set, it is acceptable to add a new option to the union if the [appropriate
284324
conventions](api-conventions.md#objects) were followed in the original object.
285325
Removing an option requires following the [deprecation process](https://kubernetes.io/docs/reference/deprecation-policy/).
286-
326+
287327
* Changing any validation rules always has the potential of breaking some client, since it changes the
288328
assumptions about part of the API, similar to adding new enum values. Validation rules on spec fields can
289329
neither be relaxed nor strengthened. Strengthening cannot be permitted because any requests that previously
290330
worked must continue to work. Weakening validation has the potential to break other consumers and generators
291331
of the API resource. Status fields whose writers are under our control (e.g., written by non-pluggable
292332
controllers), may potentially tighten validation, since that would cause a subset of previously valid
293333
values to be observable by clients.
294-
334+
295335
* Do not add a new API version of an existing resource and make it the preferred version in the same
296336
release, and do not make it the storage version. The latter is necessary so that a rollback of the
297337
apiserver doesn't render resources in etcd undecodable after rollback.
@@ -308,16 +348,15 @@ was added.
308348

309349
## Incompatible API changes
310350

311-
There are times when this might be OK, but mostly we want changes that meet this
312-
definition. If you think you need to break compatibility, you should talk to the
313-
Kubernetes team first.
351+
There are times when incompatible changes might be OK, but mostly we want
352+
changes that meet the above definitions. If you think you need to break
353+
compatibility, you should talk to the Kubernetes API reviewers first.
314354

315355
Breaking compatibility of a beta or stable API version, such as v1, is
316356
unacceptable. Compatibility for experimental or alpha APIs is not strictly
317357
required, but breaking compatibility should not be done lightly, as it disrupts
318-
all users of the feature. Experimental APIs may be removed. Alpha and beta API
319-
versions may be deprecated and eventually removed wholesale, as described in the
320-
[versioning document](../design-proposals/release/versioning.md).
358+
all users of the feature. Alpha and beta API versions may be deprecated and
359+
eventually removed wholesale, as described in the [deprecation policy](https://kubernetes.io/docs/reference/deprecation-policy/).
321360

322361
If your change is going to be backward incompatible or might be a breaking
323362
change for API consumers, please send an announcement to

0 commit comments

Comments
 (0)