@@ -9,6 +9,10 @@ found at [API Conventions](api-conventions.md).
9
9
- [ So you want to change the API?] ( #so-you-want-to-change-the-api )
10
10
- [ Operational overview] ( #operational-overview )
11
11
- [ On compatibility] ( #on-compatibility )
12
+ - [ Adding a field] ( #adding-a-field )
13
+ - [ Making a singular field plural] ( #making-a-singular-field-plural )
14
+ - [ Single-Dual ambiguity] ( #single-dual-ambiguity )
15
+ - [ Multiple API versions] ( multiple-api-versions )
12
16
- [ Backward compatibility gotchas] ( #backward-compatibility-gotchas )
13
17
- [ Incompatible API changes] ( #incompatible-api-changes )
14
18
- [ Changing versioned APIs] ( #changing-versioned-apis )
@@ -144,6 +148,8 @@ made in extreme cases (e.g. security or obvious bugs).
144
148
145
149
Let's consider some examples.
146
150
151
+ ### Adding a field
152
+
147
153
In a hypothetical API (assume we're at version v6), the ` Frobber ` struct looks
148
154
something like this:
149
155
@@ -171,6 +177,8 @@ The onus is on you to define a sane default value for `Width` such that rules
171
177
#1 and #2 above are true - API calls and stored objects that used to work must
172
178
continue to work.
173
179
180
+ ### Making a singular field plural
181
+
174
182
For your next change you want to allow multiple ` Param ` values. You can not
175
183
simply remove ` Param string ` and add ` Params []string ` (without creating a
176
184
whole new API version) - that fails rules #1 , #2 , #3 , and #6 . Nor can you
@@ -191,24 +199,105 @@ type Frobber struct {
191
199
192
200
This new field must be inclusive of the singular field. In order to satisfy
193
201
the compatibility rules you must handle all the cases of version skew, multiple
194
- clients, and rollbacks. This can be handled by defaulting or admission control
195
- logic linking the fields together with context from the API operation to get as
196
- close as possible to the user's intentions.
202
+ clients, and rollbacks. This can be handled by admission control or API
203
+ registry logic (e.g. strategy) linking the fields together with context from
204
+ the API operation to get as close as possible to the user's intentions.
197
205
198
- Upon any mutating API operation:
206
+ Upon any read operation:
207
+ * If plural is not populated, API logic must populate plural as a one-element
208
+ list, with plural[ 0] set to the singular value.
209
+
210
+ Upon any create operation:
199
211
* If only the singular field is specified (e.g. an older client), API logic
200
- must populate plural[ 0] from the singular value, and de-dup the plural
201
- field.
202
- * If only the plural field is specified (e.g. a newer client), API logic must
203
- populate the singular value from plural[ 0] .
212
+ must populate plural as a one-element list, with plural[ 0] set to the
213
+ singular value. Rationale: It's an old client and they get compatible
214
+ behavior.
204
215
* If both the singular and plural fields are specified, API logic must
205
- validate that the singular value matches plural[ 0] .
206
- * Any other case is an error and must be rejected.
216
+ validate that plural[ 0] matches the singular value.
217
+ * Any other case is an error and must be rejected. This includes the case of
218
+ the plural field being specified and the singular not. Rationale: In an
219
+ update, it's impossible to tell the difference between an old client
220
+ clearing the singular field via patch and a new client setting the plural
221
+ field. For compatibility, we must assume the former, and we don't want
222
+ update semantics to differ from create (see [ Single-Dual
223
+ ambiguity] ( #single_dual_ambiguity ) below.
224
+
225
+ For the above: "is specified" means the field is present in the user-provided
226
+ input (including defaulted fields).
227
+
228
+ Upon any update operation (including patch):
229
+ * If singular is cleared and plural is not changed, API logic must clear
230
+ plural. Rationale: It's an old client clearing the field it knows about.
231
+ * If plural is cleared and singular is not changed, API logic must populate
232
+ the new plural with the same values as the old. Rationale: It's an old
233
+ client which can't send fields it doesn't know about.
234
+ * If the singular field is changed (but not cleared) and the plural field is
235
+ not changed, API logic must populate plural as a one-element list, with
236
+ plural[ 0] set to the singular value. Rationale: It's an old client
237
+ changing the field they know about.
238
+
239
+ Expressed as code, this looks like the following:
207
240
208
- For this purpose "is specified" means the following:
209
- * On a create or patch operation: the field is present in the user-provided input
210
- * On an update operation: the field is present and has changed from the
211
- current value
241
+ ```
242
+ // normalizeParams adjusts Params based on Param. This must not consider
243
+ // any other fields.
244
+ func normalizeParams(after, before *api.Frobber) {
245
+ // Validation will be called on the new object soon enough. All this
246
+ // needs to do is try to divine what user meant with these linked fields.
247
+ // The below is verbosely written for clarity.
248
+
249
+ // **** IMPORTANT *****
250
+ // As a governing rule. User must either:
251
+ // a) Use singular field only (old client)
252
+ // b) Use singular *and* plural fields (new client)
253
+
254
+ if before == nil {
255
+ // This was a create operation.
256
+
257
+ // User specified singular and not plural (an old client), so we can
258
+ // init plural for them.
259
+ if len(after.Param) > 0 && len(after.Params) == 0 {
260
+ after.Params = []string{after.Param}
261
+ return
262
+ }
263
+
264
+ // Either both were specified or both were not. Catch this in
265
+ // validation.
266
+ return
267
+ }
268
+
269
+ // This was an update operation.
270
+
271
+ // Plural was cleared by an old client which was trying to patch
272
+ // some field and didn't provide it.
273
+ if len(before.Params) > 0 && len(after.Params) == 0 {
274
+ // If singular is unchanged, then it is an old client trying to
275
+ // patch, and didn't provide plural. Bring the old value forward.
276
+ if before.Param == after.Param {
277
+ after.Params = before.Params
278
+ }
279
+ }
280
+
281
+ if before.Param != after.Param {
282
+ // Singular is changed.
283
+
284
+ if len(before.Param) > 0 && len(after.Param) == 0 {
285
+ // If singular was cleared and plural is unchanged, then we can
286
+ // clear plural to match.
287
+ if sameStringSlice(before.Params, after.Params) {
288
+ after.Params = nil
289
+ }
290
+ // Else they also changed plural - check it in validation.
291
+ } else {
292
+ // If singular was changed (but not cleared) and plural was not,
293
+ // then we can set plural based on singular (same as create).
294
+ if sameStringSlice(before.Params, after.Params) {
295
+ after.Params = []string{after.Param}
296
+ }
297
+ }
298
+ }
299
+ }
300
+ ```
212
301
213
302
Older clients that only know the singular field will continue to succeed and
214
303
produce the same results as before the change. Newer clients can use your
@@ -232,9 +321,68 @@ The code that converts to/from versioned APIs can decode this into the
232
321
compatible structure. Eventually, a new API version, e.g. v7beta1,
233
322
will be forked and it can drop the singular field entirely.
234
323
324
+ #### Single-Dual ambiguity
325
+
326
+ Assume the user starts with:
327
+
328
+ ```
329
+ kind: Frobber
330
+ height: 42
331
+ width: 3
332
+ param: "super"
333
+ ```
334
+
335
+ On create we can set ` params: ["super"] ` .
336
+
337
+ On an unrelated POST (aka replace), an old client would send:
338
+
339
+ ```
340
+ kind: Frobber
341
+ height: 3
342
+ width: 42
343
+ param: "super"
344
+ ```
345
+
346
+ If we don't require new clients to use both singular and plural fields, a new
347
+ client would send:
348
+
349
+ ```
350
+ kind: Frobber
351
+ height: 3
352
+ width: 42
353
+ params: ["super"]
354
+ ```
355
+
356
+ That seems clear enough - we can assume ` param: "super" ` .
357
+
358
+ But the old client could send this, via patch:
359
+
360
+ ```
361
+ PATCH /frobbers/1
362
+ { param: "" }
363
+ ```
364
+
365
+ That gets applied to the old object before registry code can see it, and we end up with:
366
+
367
+ ```
368
+ kind: Frobber
369
+ height: 42
370
+ width: 3
371
+ params: ["super"]
372
+ ```
373
+
374
+ By the previous logic, we would copy ` params[0] ` to ` param ` and end up with
375
+ ` param: "super" ` . But that's not what the user wanted and more importantly is
376
+ different than what happened before we pluralized.
377
+
378
+ To disambiguate that, we require users of plural to always specify singular,
379
+ too.
380
+
381
+ ### Multiple API versions
382
+
235
383
We've seen how to satisfy rules #1 , #2 , and #3 . Rule #4 means that you can not
236
384
extend one versioned API without also extending the others. For example, an
237
- API call might POST an object in API v7beta1 format, which uses the cleaner
385
+ API call might POST an object in API v7beta1 format, which uses the new
238
386
` Params ` field, but the API server might store that object in trusty old v6
239
387
form (since v7beta1 is "beta"). When the user reads the object back in the
240
388
v7beta1 API it would be unacceptable to have lost all but ` Params[0] ` . This
0 commit comments