From 1055eed9b6829347a568eceea51f3662ea6862a6 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Mon, 27 Jul 2015 12:49:06 -0700 Subject: [PATCH] Add initial storage types to the Kubernetes API --- docs/design/extending-api.md | 222 +++++++++++++++++++++++++++++ pkg/api/deep_copy_generated.go | 69 +++++++++ pkg/api/latest/latest.go | 5 +- pkg/api/register.go | 6 + pkg/api/types.go | 32 +++++ pkg/api/v1/conversion_generated.go | 152 ++++++++++++++++++++ pkg/api/v1/deep_copy_generated.go | 69 +++++++++ pkg/api/v1/register.go | 6 + pkg/api/v1/types.go | 32 +++++ pkg/api/validation/validation.go | 23 +++ 10 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 docs/design/extending-api.md diff --git a/docs/design/extending-api.md b/docs/design/extending-api.md new file mode 100644 index 00000000000..cca257bd195 --- /dev/null +++ b/docs/design/extending-api.md @@ -0,0 +1,222 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +If you are using a released version of Kubernetes, you should +refer to the docs that go with that version. + + +The latest 1.0.x release of this document can be found +[here](http://releases.k8s.io/release-1.0/docs/design/extending-api.md). + +Documentation for other releases can be found at +[releases.k8s.io](http://releases.k8s.io). + +-- + + + + + +# Adding custom resources to the Kubernetes API server + +This document describes the design for implementing the storage of custom API types in the Kubernetes API Server. + + +## Resource Model + +### The ThirdPartyResource + +The `ThirdPartyResource` resource describes the multiple versions of a custom resource that the user wants to add +to the Kubernetes API. `ThirdPartyResource` is a non-namespaced resource, attempting to place it in a resource +will return an error. + +Each `ThirdPartyResource` resource has the following: + * Standard Kubernetes object metadata. + * ResourceKind - The kind of the resources described by this third party resource. + * Description - A free text description of the resource. + * APIGroup - An API group that this resource should be placed into. + * Versions - One or more `Version` objects. + +### The `Version` Object + +The `Version` object describes a single concrete version of a custom resource. The `Version` object currently +only specifies: + * The `Name` of the version. + * The `APIGroup` this version should belong to. + +## Expectations about third party objects + +Every object that is added to a third-party Kubernetes object store is expected to contain Kubernetes +compatible [object metadata](../devel/api-conventions.md#metadata). This requirement enables the +Kubernetes API server to provide the following features: + * Filtering lists of objects via LabelQueries + * `resourceVersion`-based optimistic concurrency via compare-and-swap + * Versioned storage + * Event recording + * Integration with basic `kubectl` command line tooling. + * Watch for resource changes. + +The `Kind` for an instance of a third-party object (e.g. CronTab) below is expected to be +programnatically convertible to the name of the resource using +the following conversion. Kinds are expected to be of the form ``, the +`APIVersion` for the object is expected to be `//`. + +For example `example.com/stable/v1` + +`domain-name` is expected to be a fully qualified domain name. + +'CamelCaseKind' is the specific type name. + +To convert this into the `metadata.name` for the `ThirdPartyResource` resource instance, +the `` is copied verbatim, the `CamelCaseKind` is +then converted +using '-' instead of capitalization ('camel-case'), with the first character being assumed to be +capitalized. In pseudo code: + +```go +var result string +for ix := range kindName { + if isCapital(kindName[ix]) { + result = append(result, '-') + } + result = append(result, toLowerCase(kindName[ix]) +} +``` + +As a concrete example, the resource named `camel-case-kind.example.com` defines resources of Kind `CamelCaseKind`, in +the APIGroup with the prefix `example.com/...`. + +The reason for this is to enable rapid lookup of a `ThirdPartyResource` object given the kind information. +This is also the reason why `ThirdPartyResource` is not namespaced. + +## Usage + +When a user creates a new `ThirdPartyResource`, the Kubernetes API Server reacts by creating a new, namespaced +RESTful resource path. For now, non-namespaced objects are not supported. As with existing built-in objects +deleting a namespace, deletes all third party resources in that namespace. + +For example, if a user creates: + +```yaml +metadata: + name: cron-tab.example.com +apiVersion: experimental/v1 +kind: ThirdPartyResource +description: "A specification of a Pod to run on a cron style schedule" +versions: + - name: stable/v1 + - name: experimental/v2 +``` + +Then the API server will program in two new RESTful resource paths: + * `/thirdparty/example.com/stable/v1/namespaces//crontabs/...` + * `/thirdparty/example.com/experimental/v2/namespaces//crontabs/...` + + +Now that this schema has been created, a user can `POST`: + +```json +{ + "metadata": { + "name": "my-new-cron-object" + }, + "apiVersion": "example.com/stable/v1", + "kind": "CronTab", + "cronSpec": "* * * * /5", + "image": "my-awesome-chron-image" +} +``` + +to: `/third-party/example.com/stable/v1/namespaces/default/crontabs/my-new-cron-object` + +and the corresponding data will be stored into etcd by the APIServer, so that when the user issues: + +``` +GET /third-party/example.com/stable/v1/namespaces/default/crontabs/my-new-cron-object` +``` + +And when they do that, they will get back the same data, but with additional Kubernetes metadata +(e.g. `resourceVersion`, `createdTimestamp`) filled in. + +Likewise, to list all resources, a user can issue: + +``` +GET /third-party/example.com/stable/v1/namespaces/default/crontabs +``` + +and get back: + +```json +{ + "apiVersion": "example.com/stable/v1", + "kind": "CronTabList", + "items": [ + { + "metadata": { + "name": "my-new-cron-object" + }, + "apiVersion": "example.com/stable/v1", + "kind": "CronTab", + "cronSpec": "* * * * /5", + "image": "my-awesome-chron-image" + } + ] +} +``` + +Because all objects are expected to contain standard Kubernetes metdata fileds, these +list operations can also use `Label` queries to filter requests down to specific subsets. + +Likewise, clients can use watch endpoints to watch for changes to stored objects. + + +## Storage + +In order to store custom user data in a versioned fashion inside of etcd, we need to also introduce a +`Codec`-compatible object for persistent storage in etcd. This object is `ThirdPartyResourceData` and it contains: + * Standard API Metadata + * `Data`: The raw JSON data for this custom object. + +### Storage key specification + +Each custom object stored by the API server needs a custom key in storage, this is described below: + +#### Definitions + + * `resource-namespace` : the namespace of the particular resource that is being stored + * `resource-name`: the name of the particular resource being stored + * `third-party-resource-namespace`: the namespace of the `ThirdPartyResource` resource that represents the type for the specific instance being stored. + * `third-party-resource-name`: the name of the `ThirdPartyResource` resource that represents the type for the specific instance being stored. + +#### Key + +Given the definitions above, the key for a specific third-party object is: + +``` +${standard-k8s-prefix}/third-party-resources/${third-party-resource-namespace}/${third-party-resource-name}/${resource-namespace}/${resource-name} +``` + +Thus, listing a third-party resource can be achieved by listing the directory: + +``` +${standard-k8s-prefix}/third-party-resources/${third-party-resource-namespace}/${third-party-resource-name}/${resource-namespace}/ +``` + + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/design/extending-api.md?pixel)]() + diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index 9898e2dc70e..0d4680f58b0 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -29,6 +29,12 @@ import ( inf "speter.net/go/exp/math/dec/inf" ) +func deepCopy_api_APIVersion(in APIVersion, out *APIVersion, c *conversion.Cloner) error { + out.Name = in.Name + out.APIGroup = in.APIGroup + return nil +} + func deepCopy_api_AWSElasticBlockStoreVolumeSource(in AWSElasticBlockStoreVolumeSource, out *AWSElasticBlockStoreVolumeSource, c *conversion.Cloner) error { out.VolumeID = in.VolumeID out.FSType = in.FSType @@ -2014,6 +2020,65 @@ func deepCopy_api_TCPSocketAction(in TCPSocketAction, out *TCPSocketAction, c *c return nil } +func deepCopy_api_ThirdPartyResource(in ThirdPartyResource, out *ThirdPartyResource, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + out.Description = in.Description + if in.Versions != nil { + out.Versions = make([]APIVersion, len(in.Versions)) + for i := range in.Versions { + if err := deepCopy_api_APIVersion(in.Versions[i], &out.Versions[i], c); err != nil { + return err + } + } + } else { + out.Versions = nil + } + return nil +} + +func deepCopy_api_ThirdPartyResourceData(in ThirdPartyResourceData, out *ThirdPartyResourceData, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if in.Data != nil { + out.Data = make([]uint8, len(in.Data)) + for i := range in.Data { + out.Data[i] = in.Data[i] + } + } else { + out.Data = nil + } + return nil +} + +func deepCopy_api_ThirdPartyResourceList(in ThirdPartyResourceList, out *ThirdPartyResourceList, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ThirdPartyResource, len(in.Items)) + for i := range in.Items { + if err := deepCopy_api_ThirdPartyResource(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func deepCopy_api_TypeMeta(in TypeMeta, out *TypeMeta, c *conversion.Cloner) error { out.Kind = in.Kind out.APIVersion = in.APIVersion @@ -2161,6 +2226,7 @@ func deepCopy_util_Time(in util.Time, out *util.Time, c *conversion.Cloner) erro func init() { err := Scheme.AddGeneratedDeepCopyFuncs( + deepCopy_api_APIVersion, deepCopy_api_AWSElasticBlockStoreVolumeSource, deepCopy_api_Binding, deepCopy_api_Capabilities, @@ -2276,6 +2342,9 @@ func init() { deepCopy_api_StatusCause, deepCopy_api_StatusDetails, deepCopy_api_TCPSocketAction, + deepCopy_api_ThirdPartyResource, + deepCopy_api_ThirdPartyResourceData, + deepCopy_api_ThirdPartyResourceList, deepCopy_api_TypeMeta, deepCopy_api_Volume, deepCopy_api_VolumeMount, diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index bc2f93b5652..29ace24796b 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -95,7 +95,10 @@ func init() { "PodExecOptions", "PodAttachOptions", "PodProxyOptions", - "Daemon") + "Daemon", + "ThirdPartyResource", + "ThirdPartyResourceData", + "ThirdPartyResourceList") mapper := api.NewDefaultRESTMapper("api", versions, InterfacesFor, importPrefix, ignoredKinds, rootScoped) // setup aliases for groups of resources diff --git a/pkg/api/register.go b/pkg/api/register.go index 8029ec64075..6a16cbacb9a 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -69,6 +69,9 @@ func init() { &ComponentStatusList{}, &SerializedReference{}, &RangeAllocation{}, + &ThirdPartyResource{}, + &ThirdPartyResourceList{}, + &ThirdPartyResourceData{}, ) // Legacy names are supported Scheme.AddKnownTypeWithName("", "Minion", &Node{}) @@ -119,3 +122,6 @@ func (*ComponentStatus) IsAnAPIObject() {} func (*ComponentStatusList) IsAnAPIObject() {} func (*SerializedReference) IsAnAPIObject() {} func (*RangeAllocation) IsAnAPIObject() {} +func (*ThirdPartyResource) IsAnAPIObject() {} +func (*ThirdPartyResourceList) IsAnAPIObject() {} +func (*ThirdPartyResourceData) IsAnAPIObject() {} diff --git a/pkg/api/types.go b/pkg/api/types.go index 3e5de52b68f..1698cda3872 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -2208,3 +2208,35 @@ type RangeAllocation struct { // a single allocated address (the fifth bit on CIDR 10.0.0.0/8 is 10.0.0.4). Data []byte `json:"data"` } + +// A ThirdPartyResource is a generic representation of a resource, it is used by add-ons and plugins to add new resource +// types to the API. It consists of one or more Versions of the api. +type ThirdPartyResource struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata"` + + Description string `json:"description,omitempty" description:"The description of this object"` + + Versions []APIVersion `json:"versions,omitempty" description:"The versions for this third party object"` +} + +type ThirdPartyResourceList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + Items []ThirdPartyResource `json:"items" description:"items is a list of schema objects"` +} + +// An APIVersion represents a single concrete version of an object model. +type APIVersion struct { + Name string `json:"name,omitempty" description:"name of this version (e.g. 'v1')"` + APIGroup string `json:"apiGroup,omitempty" description:"The API group to add this object into, default 'experimental'"` +} + +// An internal object, used for versioned storage in etcd. Not exposed to the end user. +type ThirdPartyResourceData struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata"` + + Data []byte `json:"name,omitempty" description:"the raw JSON data for this data"` +} diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index e94609a6a12..07b884d0039 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -25,6 +25,15 @@ import ( conversion "k8s.io/kubernetes/pkg/conversion" ) +func convert_api_APIVersion_To_v1_APIVersion(in *api.APIVersion, out *APIVersion, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.APIVersion))(in) + } + out.Name = in.Name + out.APIGroup = in.APIGroup + return nil +} + func convert_api_AWSElasticBlockStoreVolumeSource_To_v1_AWSElasticBlockStoreVolumeSource(in *api.AWSElasticBlockStoreVolumeSource, out *AWSElasticBlockStoreVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.AWSElasticBlockStoreVolumeSource))(in) @@ -2243,6 +2252,69 @@ func convert_api_TCPSocketAction_To_v1_TCPSocketAction(in *api.TCPSocketAction, return nil } +func convert_api_ThirdPartyResource_To_v1_ThirdPartyResource(in *api.ThirdPartyResource, out *ThirdPartyResource, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.ThirdPartyResource))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ObjectMeta_To_v1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + out.Description = in.Description + if in.Versions != nil { + out.Versions = make([]APIVersion, len(in.Versions)) + for i := range in.Versions { + if err := convert_api_APIVersion_To_v1_APIVersion(&in.Versions[i], &out.Versions[i], s); err != nil { + return err + } + } + } else { + out.Versions = nil + } + return nil +} + +func convert_api_ThirdPartyResourceData_To_v1_ThirdPartyResourceData(in *api.ThirdPartyResourceData, out *ThirdPartyResourceData, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.ThirdPartyResourceData))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ObjectMeta_To_v1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := s.Convert(&in.Data, &out.Data, 0); err != nil { + return err + } + return nil +} + +func convert_api_ThirdPartyResourceList_To_v1_ThirdPartyResourceList(in *api.ThirdPartyResourceList, out *ThirdPartyResourceList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.ThirdPartyResourceList))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ListMeta_To_v1_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ThirdPartyResource, len(in.Items)) + for i := range in.Items { + if err := convert_api_ThirdPartyResource_To_v1_ThirdPartyResource(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func convert_api_TypeMeta_To_v1_TypeMeta(in *api.TypeMeta, out *TypeMeta, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.TypeMeta))(in) @@ -2368,6 +2440,15 @@ func convert_api_VolumeSource_To_v1_VolumeSource(in *api.VolumeSource, out *Volu return nil } +func convert_v1_APIVersion_To_api_APIVersion(in *APIVersion, out *api.APIVersion, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*APIVersion))(in) + } + out.Name = in.Name + out.APIGroup = in.APIGroup + return nil +} + func convert_v1_AWSElasticBlockStoreVolumeSource_To_api_AWSElasticBlockStoreVolumeSource(in *AWSElasticBlockStoreVolumeSource, out *api.AWSElasticBlockStoreVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*AWSElasticBlockStoreVolumeSource))(in) @@ -4586,6 +4667,69 @@ func convert_v1_TCPSocketAction_To_api_TCPSocketAction(in *TCPSocketAction, out return nil } +func convert_v1_ThirdPartyResource_To_api_ThirdPartyResource(in *ThirdPartyResource, out *api.ThirdPartyResource, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ThirdPartyResource))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + out.Description = in.Description + if in.Versions != nil { + out.Versions = make([]api.APIVersion, len(in.Versions)) + for i := range in.Versions { + if err := convert_v1_APIVersion_To_api_APIVersion(&in.Versions[i], &out.Versions[i], s); err != nil { + return err + } + } + } else { + out.Versions = nil + } + return nil +} + +func convert_v1_ThirdPartyResourceData_To_api_ThirdPartyResourceData(in *ThirdPartyResourceData, out *api.ThirdPartyResourceData, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ThirdPartyResourceData))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := s.Convert(&in.Data, &out.Data, 0); err != nil { + return err + } + return nil +} + +func convert_v1_ThirdPartyResourceList_To_api_ThirdPartyResourceList(in *ThirdPartyResourceList, out *api.ThirdPartyResourceList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ThirdPartyResourceList))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ListMeta_To_api_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]api.ThirdPartyResource, len(in.Items)) + for i := range in.Items { + if err := convert_v1_ThirdPartyResource_To_api_ThirdPartyResource(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func convert_v1_TypeMeta_To_api_TypeMeta(in *TypeMeta, out *api.TypeMeta, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*TypeMeta))(in) @@ -4713,6 +4857,7 @@ func convert_v1_VolumeSource_To_api_VolumeSource(in *VolumeSource, out *api.Volu func init() { err := api.Scheme.AddGeneratedConversionFuncs( + convert_api_APIVersion_To_v1_APIVersion, convert_api_AWSElasticBlockStoreVolumeSource_To_v1_AWSElasticBlockStoreVolumeSource, convert_api_Binding_To_v1_Binding, convert_api_Capabilities_To_v1_Capabilities, @@ -4826,10 +4971,14 @@ func init() { convert_api_StatusDetails_To_v1_StatusDetails, convert_api_Status_To_v1_Status, convert_api_TCPSocketAction_To_v1_TCPSocketAction, + convert_api_ThirdPartyResourceData_To_v1_ThirdPartyResourceData, + convert_api_ThirdPartyResourceList_To_v1_ThirdPartyResourceList, + convert_api_ThirdPartyResource_To_v1_ThirdPartyResource, convert_api_TypeMeta_To_v1_TypeMeta, convert_api_VolumeMount_To_v1_VolumeMount, convert_api_VolumeSource_To_v1_VolumeSource, convert_api_Volume_To_v1_Volume, + convert_v1_APIVersion_To_api_APIVersion, convert_v1_AWSElasticBlockStoreVolumeSource_To_api_AWSElasticBlockStoreVolumeSource, convert_v1_Binding_To_api_Binding, convert_v1_Capabilities_To_api_Capabilities, @@ -4943,6 +5092,9 @@ func init() { convert_v1_StatusDetails_To_api_StatusDetails, convert_v1_Status_To_api_Status, convert_v1_TCPSocketAction_To_api_TCPSocketAction, + convert_v1_ThirdPartyResourceData_To_api_ThirdPartyResourceData, + convert_v1_ThirdPartyResourceList_To_api_ThirdPartyResourceList, + convert_v1_ThirdPartyResource_To_api_ThirdPartyResource, convert_v1_TypeMeta_To_api_TypeMeta, convert_v1_VolumeMount_To_api_VolumeMount, convert_v1_VolumeSource_To_api_VolumeSource, diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 2d6f64c6ae9..659a0435ce4 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -44,6 +44,12 @@ func deepCopy_resource_Quantity(in resource.Quantity, out *resource.Quantity, c return nil } +func deepCopy_v1_APIVersion(in APIVersion, out *APIVersion, c *conversion.Cloner) error { + out.Name = in.Name + out.APIGroup = in.APIGroup + return nil +} + func deepCopy_v1_AWSElasticBlockStoreVolumeSource(in AWSElasticBlockStoreVolumeSource, out *AWSElasticBlockStoreVolumeSource, c *conversion.Cloner) error { out.VolumeID = in.VolumeID out.FSType = in.FSType @@ -2019,6 +2025,65 @@ func deepCopy_v1_TCPSocketAction(in TCPSocketAction, out *TCPSocketAction, c *co return nil } +func deepCopy_v1_ThirdPartyResource(in ThirdPartyResource, out *ThirdPartyResource, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + out.Description = in.Description + if in.Versions != nil { + out.Versions = make([]APIVersion, len(in.Versions)) + for i := range in.Versions { + if err := deepCopy_v1_APIVersion(in.Versions[i], &out.Versions[i], c); err != nil { + return err + } + } + } else { + out.Versions = nil + } + return nil +} + +func deepCopy_v1_ThirdPartyResourceData(in ThirdPartyResourceData, out *ThirdPartyResourceData, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if in.Data != nil { + out.Data = make([]uint8, len(in.Data)) + for i := range in.Data { + out.Data[i] = in.Data[i] + } + } else { + out.Data = nil + } + return nil +} + +func deepCopy_v1_ThirdPartyResourceList(in ThirdPartyResourceList, out *ThirdPartyResourceList, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ThirdPartyResource, len(in.Items)) + for i := range in.Items { + if err := deepCopy_v1_ThirdPartyResource(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func deepCopy_v1_TypeMeta(in TypeMeta, out *TypeMeta, c *conversion.Cloner) error { out.Kind = in.Kind out.APIVersion = in.APIVersion @@ -2163,6 +2228,7 @@ func deepCopy_util_Time(in util.Time, out *util.Time, c *conversion.Cloner) erro func init() { err := api.Scheme.AddGeneratedDeepCopyFuncs( deepCopy_resource_Quantity, + deepCopy_v1_APIVersion, deepCopy_v1_AWSElasticBlockStoreVolumeSource, deepCopy_v1_Binding, deepCopy_v1_Capabilities, @@ -2278,6 +2344,9 @@ func init() { deepCopy_v1_StatusCause, deepCopy_v1_StatusDetails, deepCopy_v1_TCPSocketAction, + deepCopy_v1_ThirdPartyResource, + deepCopy_v1_ThirdPartyResourceData, + deepCopy_v1_ThirdPartyResourceList, deepCopy_v1_TypeMeta, deepCopy_v1_Volume, deepCopy_v1_VolumeMount, diff --git a/pkg/api/v1/register.go b/pkg/api/v1/register.go index 3c0a52331c4..6dd94cf2d61 100644 --- a/pkg/api/v1/register.go +++ b/pkg/api/v1/register.go @@ -84,6 +84,9 @@ func addKnownTypes() { &ComponentStatusList{}, &SerializedReference{}, &RangeAllocation{}, + &ThirdPartyResource{}, + &ThirdPartyResourceList{}, + &ThirdPartyResourceData{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1", "Minion", &Node{}) @@ -134,3 +137,6 @@ func (*ComponentStatus) IsAnAPIObject() {} func (*ComponentStatusList) IsAnAPIObject() {} func (*SerializedReference) IsAnAPIObject() {} func (*RangeAllocation) IsAnAPIObject() {} +func (*ThirdPartyResource) IsAnAPIObject() {} +func (*ThirdPartyResourceList) IsAnAPIObject() {} +func (*ThirdPartyResourceData) IsAnAPIObject() {} diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index d91758d0a6e..40277537572 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -2060,3 +2060,35 @@ type RangeAllocation struct { Range string `json:"range" description:"a range string that identifies the range represented by 'data'; required"` Data []byte `json:"data" description:"a bit array containing all allocated addresses in the previous segment"` } + +// A ThirdPartyResource is a generic representation of a resource, it is used by add-ons and plugins to add new resource +// types to the API. It consists of one or more Versions of the api. +type ThirdPartyResource struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata"` + + Description string `json:"description,omitempty" description:"The description of this object"` + + Versions []APIVersion `json:"versions,omitempty" description:"The versions for this third party object"` +} + +type ThirdPartyResourceList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + Items []ThirdPartyResource `json:"items" description:"items is a list of schema objects"` +} + +// An APIVersion represents a single concrete version of an object model. +type APIVersion struct { + Name string `json:"name,omitempty" description:"name of this version (e.g. 'v1')"` + APIGroup string `json:"apiGroup,omitempty" description:"The API group to add this object into, default 'experimental'"` +} + +// An internal object, used for versioned storage in etcd. Not exposed to the end user. +type ThirdPartyResourceData struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata"` + + Data []byte `json:"name,omitempty" description:"the raw JSON data for this data"` +} diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index da66f051341..ef8ce7e4308 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1802,3 +1802,26 @@ func ValidateSecurityContext(sc *api.SecurityContext) errs.ValidationErrorList { } return allErrs } + +func ValidateThirdPartyResource(obj *api.ThirdPartyResource) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if len(obj.Name) == 0 { + allErrs = append(allErrs, errs.NewFieldInvalid("name", obj.Name, "name must be non-empty")) + } + versions := util.StringSet{} + for ix := range obj.Versions { + version := &obj.Versions[ix] + if len(version.Name) == 0 { + allErrs = append(allErrs, errs.NewFieldInvalid("name", version, "name can not be empty")) + } + if versions.Has(version.Name) { + allErrs = append(allErrs, errs.NewFieldDuplicate("version", version)) + } + versions.Insert(version.Name) + } + return allErrs +} + +func ValidateSchemaUpdate(oldResource, newResource *api.ThirdPartyResource) errs.ValidationErrorList { + return errs.ValidationErrorList{fmt.Errorf("Schema update is not supported.")} +}