*This document is oriented at developers who want to change existing APIs. A set of API conventions, which applies to new APIs and to changes, can be found at API Conventions.
Table of Contents
Before attempting a change to the API, you should familiarize yourself with a number of existing API types and with the API conventions. If creating a new API type/resource, we also recommend that you first send a PR containing just a proposal for the new API types, and that you initially target the extensions API (pkg/apis/extensions).
The Kubernetes API has two major components - the internal structures and the versioned APIs. The versioned APIs are intended to be stable, while the internal structures are implemented to best reflect the needs of the Kubernetes code itself.
What this means for API changes is that you have to be somewhat thoughtful in how you approach changes, and that you have to touch a number of pieces to make a complete change. This document aims to guide you through the process, though not all API changes will need all of these steps.
It is important to have a high level understanding of the API system used in Kubernetes in order to navigate the rest of this document.
As mentioned above, the internal representation of an API object is decoupled from any one API version. This provides a lot of freedom to evolve the code, but it requires robust infrastructure to convert between representations. There are multiple steps in processing an API operation - even something as simple as a GET involves a great deal of machinery.
The conversion process is logically a "star" with the internal form at the center. Every versioned API can be converted to the internal form (and vice-versa), but versioned APIs do not convert to other versioned APIs directly. This sounds like a heavy process, but in reality we do not intend to keep more than a small number of versions alive at once. While all of the Kubernetes code operates on the internal structures, they are always converted to a versioned form before being written to storage (disk or etcd) or being sent over a wire. Clients should consume and operate on the versioned APIs exclusively.
To demonstrate the general process, here is a (hypothetical) example:
Pod
object to /api/v7beta1/...
v7beta1.Pod
structurev7beta1.Pod
v7beta1.Pod
is converted to an api.Pod
structureapi.Pod
is validated, and any errors are returned to the userapi.Pod
is converted to a v6.Pod
(because v6 is the latest stable
version)v6.Pod
is marshalled into JSON and written to etcdNow that we have the Pod
object stored, a user can GET that object in any
supported api version. For example:
Pod
from /api/v5/...
v6.Pod
structurev6.Pod
v6.Pod
is converted to an api.Pod
structureapi.Pod
is converted to a v5.Pod
structurev5.Pod
is marshalled into JSON and sent to the userThe implication of this process is that API changes must be done carefully and backward-compatibly.
Before talking about how to make API changes, it is worthwhile to clarify what we mean by API compatibility. An API change is considered backward-compatible if it:
Put another way:
If your change does not meet these criteria, it is not considered strictly compatible.
Let's consider some examples. In a hypothetical API (assume we're at version
v6), the Frobber
struct looks something like this:
// API v6.
type Frobber struct {
Height int `json:"height"`
Param string `json:"param"`
}
You want to add a new Width
field. It is generally safe to add new fields
without changing the API version, so you can simply change it to:
// Still API v6.
type Frobber struct {
Height int `json:"height"`
Width int `json:"width"`
Param string `json:"param"`
}
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.
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:
// Still API v6, but kind of clumsy.
type Frobber struct {
Height int `json:"height"`
Width int `json:"width"`
Param string `json:"param"` // the first param
ExtraParams []string `json:"extraParams"` // additional 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 distinct from any one version is to handle growth like this. The internal representation can be implemented as:
// Internal, soon to be v7beta1.
type Frobber struct {
Height int
Width int
Params []string
}
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.
We've seen how to satisfy rules #1 and #2. Rule #3 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.
However, this is very challenging to do correctly. It often requires 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:
type Frobber struct {
Height *int `json:"height"`
Width *int `json:"width"`
HeightInInches *int `json:"heightInInches"`
WidthInInches *int `json:"widthInInches"`
}
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)?
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:
{
"height": 10,
"width": 5
}
and GETs:
{
"height": 10,
"heightInInches": 10,
"width": 5,
"widthInInches": 5
}
then PUTs back:
{
"height": 13,
"heightInInches": 10,
"width": 5,
"widthInInches": 5
}
The update should not fail, because it would have worked before heightInInches
was added.
Therefore, when there are duplicate fields, the old field MUST take precedence over the new, and the new field should be set to match by the server upon write. A new client would be aware of the old field as well as the new, and so can ensure that the old field is either unset or is set consistently with the new field. However, older clients would be unaware of the new field. Please avoid introducing duplicate fields due to the complexity they incur in the API.
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 old representation would not be aware of the new representation nor its semantics. Examples of proposals that have run into this challenge include generalized label selectors and pod-level security context.
As another interesting example, enumerated values cause similar challenges. Adding a new value to an enumerated set is not a compatible change. Clients which assume they know how to handle all possible values of a given field will not be able to handle the new values. However, removing value from an enumerated set can be a compatible change, if handled properly (treat the removed value as deprecated but allowed). This is actually a special case of a new representation, discussed above.
For Unions, sets of fields where at most one should be set, it is acceptable to add a new option to the union if the appropriate conventions were followed in the original object. Removing an option requires following the deprecation process.
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.
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. Document incompatible changes across API versions under the appropriate {v? conversion tips tag in the api.md doc.
If your change is going to be backward incompatible or might be a breaking
change for API consumers, please send an announcement to
kubernetes-dev@googlegroups.com
before the change gets in. If you are unsure,
ask. Also make sure that the change gets documented in the release notes for the
next release by labeling the PR with the "release-note" github label.
If you found that your change accidentally broke clients, it should be reverted.
In short, the expected API evolution is as follows:
extensions/v1alpha1
->newapigroup/v1alpha1
-> ... -> newapigroup/v1alphaN
->newapigroup/v1beta1
-> ... -> newapigroup/v1betaN
->newapigroup/v1
->newapigroup/v2alpha1
-> ...While in extensions we have no obligation to move forward with the API at all and may delete or break it at any time.
While in alpha we expect to move forward with it, but may break it.
Once in beta we will preserve forward compatibility, but may introduce new versions and delete old ones.
v1 must be backward-compatible for an extended length of time.
For most changes, you will probably find it easiest to change the versioned APIs first. This forces you to think about how to make your change in a compatible way. Rather than doing each step in every version, it's usually easier to do each versioned API one at a time, or to do all of one version before starting "all the rest".
The struct definitions for each API are in pkg/api/<version>/types.go
. Edit
those files to reflect the change you want to make. Note that all types and
non-inline fields in versioned APIs must be preceded by descriptive comments -
these are used to generate documentation. Comments for types should not contain
the type name; API documentation is generated from these comments and end-users
should not be exposed to golang type names.
Optional fields should have the ,omitempty
json tag; fields are interpreted as
being required otherwise.
If your change includes new fields for which you will need default values, you
need to add cases to pkg/api/<version>/defaults.go
. Of course, since you
have added code, you have to add a test: pkg/api/<version>/defaults_test.go
.
Do use pointers to scalars when you need to distinguish between an unset value
and an automatic zero value. For example,
PodSpec.TerminationGracePeriodSeconds
is defined as *int64
the go type
definition. A zero value means 0 seconds, and a nil value asks the system to
pick a default.
Don't forget to run the tests!
Given that you have not yet changed the internal structs, this might feel
premature, and that's because it is. You don't yet have anything to convert to
or from. We will revisit this in the "internal" section. If you're doing this
all in a different order (i.e. you started with the internal structs), then you
should jump to that topic below. In the very rare case that you are making an
incompatible change you might or might not want to do this now, but you will
have to do more later. The files you want are
pkg/api/<version>/conversion.go
and pkg/api/<version>/conversion_test.go
.
Note that the conversion machinery doesn't generically handle conversion of values, such as various kinds of field references and API constants. The client library has custom conversion code for field references. You also need to add a call to api.Scheme.AddFieldLabelConversionFunc with a mapping function that understands supported translations.
Now it is time to change the internal structs so your versioned changes can be used.
Similar to the versioned APIs, the definitions for the internal structs are in
pkg/api/types.go
. Edit those files to reflect the change you want to make.
Keep in mind that the internal structs must be able to express all of the
versioned APIs.
Most changes made to the internal structs need some form of input validation.
Validation is currently done on internal objects in
pkg/api/validation/validation.go
. This validation is the one of the first
opportunities we have to make a great user experience - good error messages and
thorough validation help ensure that users are giving you what you expect and,
when they don't, that they know why and how to fix it. Think hard about the
contents of string
fields, the bounds of int
fields and the
requiredness/optionalness of fields.
Of course, code needs tests - pkg/api/validation/validation_test.go
.
At this point you have both the versioned API changes and the internal
structure changes done. If there are any notable differences - field names,
types, structural change in particular - you must add some logic to convert
versioned APIs to and from the internal representation. If you see errors from
the serialization_test
, it may indicate the need for explicit conversions.
Performance of conversions very heavily influence performance of apiserver. Thus, we are auto-generating conversion functions that are much more efficient than the generic ones (which are based on reflections and thus are highly inefficient).
The conversion code resides with each versioned API. There are two files:
pkg/api/<version>/conversion.go
containing manually written conversion
functionspkg/api/<version>/conversion_generated.go
containing auto-generated
conversion functionspkg/apis/extensions/<version>/conversion.go
containing manually written
conversion functionspkg/apis/extensions/<version>/conversion_generated.go
containing
auto-generated conversion functionsSince auto-generated conversion functions are using manually written ones,
those manually written should be named with a defined convention, i.e. a
function converting type X in pkg a to type Y in pkg b, should be named:
convert_a_X_To_b_Y
.
Also note that you can (and for efficiency reasons should) use auto-generated conversion functions when writing your conversion functions.
Once all the necessary manually written conversions are added, you need to regenerate auto-generated ones. To regenerate them run:
hack/update-codegen.sh
As part of the build, kubernetes will also generate code to handle deep copy of your versioned api objects. The deep copy code resides with each versioned API:
<path_to_versioned_api>/zz_generated.deepcopy.go
containing auto-generated copy functionsIf regeneration is somehow not possible due to compile errors, the easiest workaround is to comment out the code causing errors and let the script to regenerate it. If the auto-generated conversion methods are not used by the manually-written ones, it's fine to just remove the whole file and let the generator to create it from scratch.
Unsurprisingly, adding manually written conversion also requires you to add
tests to pkg/api/<version>/conversion_test.go
.
For any core API object, we also need to generate the Protobuf IDL and marshallers. That generation is done with
hack/update-generated-protobuf.sh
The vast majority of objects will not need any consideration when converting
to protobuf, but be aware that if you depend on a Golang type in the standard
library there may be additional work required, although in practice we typically
use our own equivalents for JSON serialization. The pkg/api/serialization_test.go
will verify that your protobuf serialization preserves all fields - be sure to
run it several times to ensure there are no incompletely calculated fields.
We are auto-generating code for marshaling and unmarshaling json representation of api objects - this is to improve the overall system performance.
The auto-generated code resides with each versioned API:
pkg/api/<version>/types.generated.go
pkg/apis/extensions/<version>/types.generated.go
To regenerate them run:
hack/update-codecgen.sh
This section is under construction, as we make the tooling completely generic.
At the moment, you'll have to make a new directory under pkg/apis/
; copy the
directory structure from pkg/apis/extensions
. Add the new group/version to all
of the hack/{verify,update}-generated-{deep-copy,conversions,swagger}.sh
files
in the appropriate places--it should just require adding your new group/version
to a bash array. See docs on adding an API group for
more.
Adding API groups outside of the pkg/apis/
directory is not currently
supported, but is clearly desirable. The deep copy & conversion generators need
to work by parsing go files instead of by reflection; then they will be easy to
point at arbitrary directories: see issue #13775.
Part of our testing regimen for APIs is to "fuzz" (fill with random values) API
objects and then convert them to and from the different API versions. This is
a great way of exposing places where you lost information or made bad
assumptions. If you have added any fields which need very careful formatting
(the test does not run validation) or if you have made assumptions such as
"this slice will always have at least 1 element", you may get an error or even
a panic from the serialization_test
. If so, look at the diff it produces (or
the backtrace in case of a panic) and figure out what you forgot. Encode that
into the fuzzer's custom fuzz functions. Hint: if you added defaults for a
field, that field will need to have a custom fuzz function that ensures that the
field is fuzzed to a non-empty value.
The fuzzer can be found in pkg/api/testing/fuzzer.go
.
VERY VERY rarely is this needed, but when it hits, it hurts. In some rare cases we end up with objects (e.g. resource quantities) that have morally equivalent values with different bitwise representations (e.g. value 10 with a base-2 formatter is the same as value 0 with a base-10 formatter). The only way Go knows how to do deep-equality is through field-by-field bitwise comparisons. This is a problem for us.
The first thing you should do is try not to do that. If you really can't avoid
this, I'd like to introduce you to our semantic DeepEqual
routine. It supports
custom overrides for specific types - you can find that in pkg/api/helpers.go
.
There's one other time when you might have to touch this: unexported fields
.
You see, while Go's reflect
package is allowed to touch unexported fields
,
us mere mortals are not - this includes semantic DeepEqual
. Fortunately, most
of our API objects are "dumb structs" all the way down - all fields are exported
(start with a capital letter) and there are no unexported fields. But sometimes
you want to include an object in our API that does have unexported fields
somewhere in it (for example, time.Time
has unexported fields). If this hits
you, you may have to touch the semantic DeepEqual
customization functions.
Now you have the API all changed - go implement whatever it is that you're doing!
Check out the E2E docs for detailed information about how to write end-to-end tests for your feature.
At last, your change is done, all unit tests pass, e2e passes, you're done,
right? Actually, no. You just changed the API. If you are touching an existing
facet of the API, you have to try really hard to make sure that all the
examples and docs are updated. There's no easy way to do this, due in part to
JSON and YAML silently dropping unknown fields. You're clever - you'll figure it
out. Put grep
or ack
to good use.
If you added functionality, you should consider documenting it and/or writing an example to illustrate your change.
Make sure you update the swagger API spec by running:
hack/update-swagger-spec.sh
The API spec changes should be in a commit separate from your other changes.
New feature development proceeds through a series of stages of increasing maturity:
alpha
(e.g. v1alpha1
)beta
(e.g. v2beta3
)v1beta1
and v1beta2
; or v1beta2
and v1
) for at least one
minor release cycle (typically 3 months) so that users have enough time to
upgrade and migrate objectsvX
where X
is an integer (e.g. v1
)When adding a feature to an object which is already Stable, the new fields and new behaviors need to meet the Stable level requirements. If these cannot be met, then the new field cannot be added to the object.
For example, consider the following object:
// API v6.
type Frobber struct {
Height int `json:"height"`
Param string `json:"param"`
}
A developer is considering adding a new Width
parameter, like this:
// API v6.
type Frobber struct {
Height int `json:"height"`
Width int `json:"height"`
Param string `json:"param"`
}
However, the new feature is not stable enough to be used in a stable version
(v6
). Some reasons for this might include:
Width
or
Breadth
?)Area()
routine sometimes overflows.)The developer cannot add the new field until stability is met. However, sometimes stability cannot be met until some users try the new feature, and some users are only able or willing to accept a released version of Kubernetes. In that case, the developer has a few options, both of which require staging work over several releases.
A preferred option is to first make a release where the new value (Width
in
this example) is specified via an annotation, like this:
kind: frobber
version: v6
metadata:
name: myfrobber
annotations:
frobbing.alpha.kubernetes.io/width: 2
height: 4
param: "green and blue"
This format allows users to specify the new field, but makes it clear that they
are using a Alpha feature when they do, since the word alpha
is in the
annotation key.
Another option is to introduce a new type with an new alpha
or beta
version
designator, like this:
// API v6alpha2
type Frobber struct {
Height int `json:"height"`
Width int `json:"height"`
Param string `json:"param"`
}
The latter requires that all objects in the same API group as Frobber
to be
replicated in the new version, v6alpha2
. This also requires user to use a new
client which uses the other version. Therefore, this is not a preferred option.
A related issue is how a cluster manager can roll back from a new version with a new feature, that is already being used by users. See https://github.com/kubernetes/kubernetes/issues/4855.