Unverified Commit 45c94a1c authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #63830 from mbohlool/crd_versioning_nop

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Crd versioning with nop Conversion Implements Custom Resource Definition versioning according to[ design doc](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/customresources-versioning.md). Note: I recreated this PR instead of #63518. Huge number of comments there broke github. @sttts @nikhita @deads2k @liggitt @lavalamp ```release-note Add CRD Versioning with NOP converter ```
parents 9c2e51f3 c25514a1
...@@ -84943,7 +84943,6 @@ ...@@ -84943,7 +84943,6 @@
"description": "CustomResourceDefinitionSpec describes how a user wants their resource to appear", "description": "CustomResourceDefinitionSpec describes how a user wants their resource to appear",
"required": [ "required": [
"group", "group",
"version",
"names", "names",
"scope" "scope"
], ],
...@@ -84969,8 +84968,15 @@ ...@@ -84969,8 +84968,15 @@
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation" "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation"
}, },
"version": { "version": {
"description": "Version is the version this resource belongs in", "description": "Version is the version this resource belongs in Should be always first item in Versions field if provided. Optional, but at least one of Version or Versions must be set. Deprecated: Please use `Versions`.",
"type": "string" "type": "string"
},
"versions": {
"description": "Versions is the list of all supported versions for this resource. If Version field is provided, this field is optional. Validation: All versions must use the same validation schema for now. i.e., top level Validation field is applied to all of these versions. Order: The version name will be used to compute the order. If the version string is \"kube-like\", it will sort above non \"kube-like\" version strings, which are ordered lexicographically. \"Kube-like\" versions start with a \"v\", then are followed by a number (the major version), then optionally the string \"alpha\" or \"beta\" and another number (the minor version). These are sorted first by GA \u003e beta \u003e alpha, and then by comparing major version, then minor version. An example sorted list of versions: v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.",
"type": "array",
"items": {
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionVersion"
}
} }
} }
}, },
...@@ -84978,7 +84984,8 @@ ...@@ -84978,7 +84984,8 @@
"description": "CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition", "description": "CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition",
"required": [ "required": [
"conditions", "conditions",
"acceptedNames" "acceptedNames",
"storedVersions"
], ],
"properties": { "properties": {
"acceptedNames": { "acceptedNames": {
...@@ -84991,6 +84998,34 @@ ...@@ -84991,6 +84998,34 @@
"items": { "items": {
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionCondition" "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionCondition"
} }
},
"storedVersions": {
"description": "StoredVersions are all versions of CustomResources that were ever persisted. Tracking these versions allows a migration path for stored versions in etcd. The field is mutable so the migration controller can first finish a migration to another version (i.e. that no old objects are left in the storage), and then remove the rest of the versions from this list. None of the versions in this list can be removed from the spec.Versions field.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionVersion": {
"required": [
"name",
"served",
"storage"
],
"properties": {
"name": {
"description": "Name is the version name, e.g. “v1”, “v2beta1”, etc.",
"type": "string"
},
"served": {
"description": "Served is a flag enabling/disabling this version from being served via REST APIs",
"type": "boolean"
},
"storage": {
"description": "Storage flags the version as storage version. There must be exactly one flagged as storage version.",
"type": "boolean"
} }
} }
}, },
...@@ -77,9 +77,11 @@ func NewAutoRegistrationController(crdinformer crdinformers.CustomResourceDefini ...@@ -77,9 +77,11 @@ func NewAutoRegistrationController(crdinformer crdinformers.CustomResourceDefini
cast := obj.(*apiextensions.CustomResourceDefinition) cast := obj.(*apiextensions.CustomResourceDefinition)
c.enqueueCRD(cast) c.enqueueCRD(cast)
}, },
UpdateFunc: func(_, obj interface{}) { UpdateFunc: func(oldObj, newObj interface{}) {
cast := obj.(*apiextensions.CustomResourceDefinition) // Enqueue both old and new object to make sure we remove and add appropriate API services.
c.enqueueCRD(cast) // The working queue will resolve any duplicates and only changes will stay in the queue.
c.enqueueCRD(oldObj.(*apiextensions.CustomResourceDefinition))
c.enqueueCRD(newObj.(*apiextensions.CustomResourceDefinition))
}, },
DeleteFunc: func(obj interface{}) { DeleteFunc: func(obj interface{}) {
cast, ok := obj.(*apiextensions.CustomResourceDefinition) cast, ok := obj.(*apiextensions.CustomResourceDefinition)
...@@ -120,8 +122,10 @@ func (c *crdRegistrationController) Run(threadiness int, stopCh <-chan struct{}) ...@@ -120,8 +122,10 @@ func (c *crdRegistrationController) Run(threadiness int, stopCh <-chan struct{})
utilruntime.HandleError(err) utilruntime.HandleError(err)
} else { } else {
for _, crd := range crds { for _, crd := range crds {
if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}); err != nil { for _, version := range crd.Spec.Versions {
utilruntime.HandleError(err) if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}); err != nil {
utilruntime.HandleError(err)
}
} }
} }
} }
...@@ -182,11 +186,12 @@ func (c *crdRegistrationController) processNextWorkItem() bool { ...@@ -182,11 +186,12 @@ func (c *crdRegistrationController) processNextWorkItem() bool {
} }
func (c *crdRegistrationController) enqueueCRD(crd *apiextensions.CustomResourceDefinition) { func (c *crdRegistrationController) enqueueCRD(crd *apiextensions.CustomResourceDefinition) {
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}) for _, version := range crd.Spec.Versions {
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name})
}
} }
func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.GroupVersion) error { func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.GroupVersion) error {
found := false
apiServiceName := groupVersion.Version + "." + groupVersion.Group apiServiceName := groupVersion.Version + "." + groupVersion.Group
// check all CRDs. There shouldn't that many, but if we have problems later we can index them // check all CRDs. There shouldn't that many, but if we have problems later we can index them
...@@ -195,26 +200,27 @@ func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.Grou ...@@ -195,26 +200,27 @@ func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.Grou
return err return err
} }
for _, crd := range crds { for _, crd := range crds {
if crd.Spec.Version == groupVersion.Version && crd.Spec.Group == groupVersion.Group { if crd.Spec.Group != groupVersion.Group {
found = true continue
break
} }
} for _, version := range crd.Spec.Versions {
if version.Name != groupVersion.Version || !version.Served {
continue
}
if !found { c.apiServiceRegistration.AddAPIServiceToSync(&apiregistration.APIService{
c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName) ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
return nil Spec: apiregistration.APIServiceSpec{
Group: groupVersion.Group,
Version: groupVersion.Version,
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
VersionPriority: 100, // CRDs will be sorted by kube-like versions like any other APIService with the same VersionPriority
},
})
return nil
}
} }
c.apiServiceRegistration.AddAPIServiceToSync(&apiregistration.APIService{ c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName)
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
Spec: apiregistration.APIServiceSpec{
Group: groupVersion.Group,
Version: groupVersion.Version,
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
VersionPriority: 100, // CRDs should have relatively low priority
},
})
return nil return nil
} }
...@@ -42,8 +42,16 @@ func TestHandleVersionUpdate(t *testing.T) { ...@@ -42,8 +42,16 @@ func TestHandleVersionUpdate(t *testing.T) {
startingCRDs: []*apiextensions.CustomResourceDefinition{ startingCRDs: []*apiextensions.CustomResourceDefinition{
{ {
Spec: apiextensions.CustomResourceDefinitionSpec{ Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com", Group: "group.com",
Version: "v1", // Version field is deprecated and crd registration won't rely on it at all.
// defaulting route will fill up Versions field if user only provided version field.
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "v1",
Served: true,
Storage: true,
},
},
}, },
}, },
}, },
...@@ -66,8 +74,14 @@ func TestHandleVersionUpdate(t *testing.T) { ...@@ -66,8 +74,14 @@ func TestHandleVersionUpdate(t *testing.T) {
startingCRDs: []*apiextensions.CustomResourceDefinition{ startingCRDs: []*apiextensions.CustomResourceDefinition{
{ {
Spec: apiextensions.CustomResourceDefinitionSpec{ Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com", Group: "group.com",
Version: "v1", Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "v1",
Served: true,
Storage: true,
},
},
}, },
}, },
}, },
...@@ -98,7 +112,6 @@ func TestHandleVersionUpdate(t *testing.T) { ...@@ -98,7 +112,6 @@ func TestHandleVersionUpdate(t *testing.T) {
t.Errorf("%s expected %v, got %v", test.name, test.expectedRemoved, registration.removed) t.Errorf("%s expected %v, got %v", test.name, test.expectedRemoved, registration.removed)
} }
} }
} }
type fakeAPIServiceRegistration struct { type fakeAPIServiceRegistration struct {
......
...@@ -119,6 +119,10 @@ ...@@ -119,6 +119,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1", "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -42,6 +42,28 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} { ...@@ -42,6 +42,28 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 { if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 {
obj.Names.ListKind = obj.Names.Kind + "List" obj.Names.ListKind = obj.Names.Kind + "List"
} }
if len(obj.Versions) == 0 && len(obj.Version) != 0 {
obj.Versions = []apiextensions.CustomResourceDefinitionVersion{
{
Name: obj.Version,
Served: true,
Storage: true,
},
}
} else if len(obj.Versions) != 0 {
obj.Version = obj.Versions[0].Name
}
},
func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) {
c.FuzzNoCustom(obj)
if len(obj.Status.StoredVersions) == 0 {
for _, v := range obj.Spec.Versions {
if v.Storage && !apiextensions.IsStoredVersion(obj, v.Name) {
obj.Status.StoredVersions = append(obj.Status.StoredVersions, v.Name)
}
}
}
}, },
func(obj *apiextensions.JSONSchemaProps, c fuzz.Continue) { func(obj *apiextensions.JSONSchemaProps, c fuzz.Continue) {
// we cannot use c.FuzzNoCustom because of the interface{} fields. So let's loop with reflection. // we cannot use c.FuzzNoCustom because of the interface{} fields. So let's loop with reflection.
......
...@@ -17,6 +17,7 @@ limitations under the License. ...@@ -17,6 +17,7 @@ limitations under the License.
package apiextensions package apiextensions
import ( import (
"fmt"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
...@@ -116,3 +117,33 @@ func CRDRemoveFinalizer(crd *CustomResourceDefinition, needle string) { ...@@ -116,3 +117,33 @@ func CRDRemoveFinalizer(crd *CustomResourceDefinition, needle string) {
} }
crd.Finalizers = newFinalizers crd.Finalizers = newFinalizers
} }
// HasServedCRDVersion returns true if `version` is in the list of CRD's versions and the Served flag is set.
func HasServedCRDVersion(crd *CustomResourceDefinition, version string) bool {
for _, v := range crd.Spec.Versions {
if v.Name == version {
return v.Served
}
}
return false
}
// GetCRDStorageVersion returns the storage version for given CRD.
func GetCRDStorageVersion(crd *CustomResourceDefinition) (string, error) {
for _, v := range crd.Spec.Versions {
if v.Storage {
return v.Name, nil
}
}
// This should not happened if crd is valid
return "", fmt.Errorf("invalid CustomResourceDefinition, no storage version")
}
func IsStoredVersion(crd *CustomResourceDefinition, version string) bool {
for _, v := range crd.Status.StoredVersions {
if version == v {
return true
}
}
return false
}
...@@ -25,6 +25,9 @@ type CustomResourceDefinitionSpec struct { ...@@ -25,6 +25,9 @@ type CustomResourceDefinitionSpec struct {
// Group is the group this resource belongs in // Group is the group this resource belongs in
Group string Group string
// Version is the version this resource belongs in // Version is the version this resource belongs in
// Should be always first item in Versions field if provided.
// Optional, but at least one of Version or Versions must be set.
// Deprecated: Please use `Versions`.
Version string Version string
// Names are the names used to describe this custom resource // Names are the names used to describe this custom resource
Names CustomResourceDefinitionNames Names CustomResourceDefinitionNames
...@@ -34,6 +37,27 @@ type CustomResourceDefinitionSpec struct { ...@@ -34,6 +37,27 @@ type CustomResourceDefinitionSpec struct {
Validation *CustomResourceValidation Validation *CustomResourceValidation
// Subresources describes the subresources for CustomResources // Subresources describes the subresources for CustomResources
Subresources *CustomResourceSubresources Subresources *CustomResourceSubresources
// Versions is the list of all supported versions for this resource.
// If Version field is provided, this field is optional.
// Validation: All versions must use the same validation schema for now. i.e., top
// level Validation field is applied to all of these versions.
// Order: The version name will be used to compute the order.
// If the version string is "kube-like", it will sort above non "kube-like" version strings, which are ordered
// lexicographically. "Kube-like" versions start with a "v", then are followed by a number (the major version),
// then optionally the string "alpha" or "beta" and another number (the minor version). These are sorted first
// by GA > beta > alpha, and then by comparing major version, then minor version. An example sorted list of
// versions: v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.
Versions []CustomResourceDefinitionVersion
}
type CustomResourceDefinitionVersion struct {
// Name is the version name, e.g. “v1”, “v2beta1”, etc.
Name string
// Served is a flag enabling/disabling this version from being served via REST APIs
Served bool
// Storage flags the version as storage version. There must be exactly one flagged
// as storage version.
Storage bool
} }
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition // CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
...@@ -115,6 +139,14 @@ type CustomResourceDefinitionStatus struct { ...@@ -115,6 +139,14 @@ type CustomResourceDefinitionStatus struct {
// AcceptedNames are the names that are actually being used to serve discovery // AcceptedNames are the names that are actually being used to serve discovery
// They may be different than the names in spec. // They may be different than the names in spec.
AcceptedNames CustomResourceDefinitionNames AcceptedNames CustomResourceDefinitionNames
// StoredVersions are all versions of CustomResources that were ever persisted. Tracking these
// versions allows a migration path for stored versions in etcd. The field is mutable
// so the migration controller can first finish a migration to another version (i.e.
// that no old objects are left in the storage), and then remove the rest of the
// versions from this list.
// None of the versions in this list can be removed from the spec.Versions field.
StoredVersions []string
} }
// CustomResourceCleanupFinalizer is the name of the finalizer which will delete instances of // CustomResourceCleanupFinalizer is the name of the finalizer which will delete instances of
......
...@@ -31,6 +31,14 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error { ...@@ -31,6 +31,14 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error {
func SetDefaults_CustomResourceDefinition(obj *CustomResourceDefinition) { func SetDefaults_CustomResourceDefinition(obj *CustomResourceDefinition) {
SetDefaults_CustomResourceDefinitionSpec(&obj.Spec) SetDefaults_CustomResourceDefinitionSpec(&obj.Spec)
if len(obj.Status.StoredVersions) == 0 {
for _, v := range obj.Spec.Versions {
if v.Storage {
obj.Status.StoredVersions = append(obj.Status.StoredVersions, v.Name)
break
}
}
}
} }
func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) { func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) {
...@@ -43,4 +51,16 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) ...@@ -43,4 +51,16 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec)
if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 { if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 {
obj.Names.ListKind = obj.Names.Kind + "List" obj.Names.ListKind = obj.Names.Kind + "List"
} }
// If there is no list of versions, create on using deprecated Version field.
if len(obj.Versions) == 0 && len(obj.Version) != 0 {
obj.Versions = []CustomResourceDefinitionVersion{{
Name: obj.Version,
Storage: true,
Served: true,
}}
}
// For backward compatibility set the version field to the first item in versions list.
if len(obj.Version) == 0 && len(obj.Versions) != 0 {
obj.Version = obj.Versions[0].Name
}
} }
...@@ -100,6 +100,10 @@ message CustomResourceDefinitionSpec { ...@@ -100,6 +100,10 @@ message CustomResourceDefinitionSpec {
optional string group = 1; optional string group = 1;
// Version is the version this resource belongs in // Version is the version this resource belongs in
// Should be always first item in Versions field if provided.
// Optional, but at least one of Version or Versions must be set.
// Deprecated: Please use `Versions`.
// +optional
optional string version = 2; optional string version = 2;
// Names are the names used to describe this custom resource // Names are the names used to describe this custom resource
...@@ -115,6 +119,18 @@ message CustomResourceDefinitionSpec { ...@@ -115,6 +119,18 @@ message CustomResourceDefinitionSpec {
// Subresources describes the subresources for CustomResources // Subresources describes the subresources for CustomResources
// +optional // +optional
optional CustomResourceSubresources subresources = 6; optional CustomResourceSubresources subresources = 6;
// Versions is the list of all supported versions for this resource.
// If Version field is provided, this field is optional.
// Validation: All versions must use the same validation schema for now. i.e., top
// level Validation field is applied to all of these versions.
// Order: The version name will be used to compute the order.
// If the version string is "kube-like", it will sort above non "kube-like" version strings, which are ordered
// lexicographically. "Kube-like" versions start with a "v", then are followed by a number (the major version),
// then optionally the string "alpha" or "beta" and another number (the minor version). These are sorted first
// by GA > beta > alpha, and then by comparing major version, then minor version. An example sorted list of
// versions: v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.
repeated CustomResourceDefinitionVersion versions = 7;
} }
// CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition // CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition
...@@ -125,6 +141,26 @@ message CustomResourceDefinitionStatus { ...@@ -125,6 +141,26 @@ message CustomResourceDefinitionStatus {
// AcceptedNames are the names that are actually being used to serve discovery // AcceptedNames are the names that are actually being used to serve discovery
// They may be different than the names in spec. // They may be different than the names in spec.
optional CustomResourceDefinitionNames acceptedNames = 2; optional CustomResourceDefinitionNames acceptedNames = 2;
// StoredVersions are all versions of CustomResources that were ever persisted. Tracking these
// versions allows a migration path for stored versions in etcd. The field is mutable
// so the migration controller can first finish a migration to another version (i.e.
// that no old objects are left in the storage), and then remove the rest of the
// versions from this list.
// None of the versions in this list can be removed from the spec.Versions field.
repeated string storedVersions = 3;
}
message CustomResourceDefinitionVersion {
// Name is the version name, e.g. “v1”, “v2beta1”, etc.
optional string name = 1;
// Served is a flag enabling/disabling this version from being served via REST APIs
optional bool served = 2;
// Storage flags the version as storage version. There must be exactly one
// flagged as storage version.
optional bool storage = 3;
} }
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources. // CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
......
...@@ -25,7 +25,11 @@ type CustomResourceDefinitionSpec struct { ...@@ -25,7 +25,11 @@ type CustomResourceDefinitionSpec struct {
// Group is the group this resource belongs in // Group is the group this resource belongs in
Group string `json:"group" protobuf:"bytes,1,opt,name=group"` Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
// Version is the version this resource belongs in // Version is the version this resource belongs in
Version string `json:"version" protobuf:"bytes,2,opt,name=version"` // Should be always first item in Versions field if provided.
// Optional, but at least one of Version or Versions must be set.
// Deprecated: Please use `Versions`.
// +optional
Version string `json:"version,omitempty" protobuf:"bytes,2,opt,name=version"`
// Names are the names used to describe this custom resource // Names are the names used to describe this custom resource
Names CustomResourceDefinitionNames `json:"names" protobuf:"bytes,3,opt,name=names"` Names CustomResourceDefinitionNames `json:"names" protobuf:"bytes,3,opt,name=names"`
// Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced // Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced
...@@ -36,6 +40,27 @@ type CustomResourceDefinitionSpec struct { ...@@ -36,6 +40,27 @@ type CustomResourceDefinitionSpec struct {
// Subresources describes the subresources for CustomResources // Subresources describes the subresources for CustomResources
// +optional // +optional
Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"` Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"`
// Versions is the list of all supported versions for this resource.
// If Version field is provided, this field is optional.
// Validation: All versions must use the same validation schema for now. i.e., top
// level Validation field is applied to all of these versions.
// Order: The version name will be used to compute the order.
// If the version string is "kube-like", it will sort above non "kube-like" version strings, which are ordered
// lexicographically. "Kube-like" versions start with a "v", then are followed by a number (the major version),
// then optionally the string "alpha" or "beta" and another number (the minor version). These are sorted first
// by GA > beta > alpha, and then by comparing major version, then minor version. An example sorted list of
// versions: v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.
Versions []CustomResourceDefinitionVersion `json:"versions,omitempty" protobuf:"bytes,7,rep,name=versions"`
}
type CustomResourceDefinitionVersion struct {
// Name is the version name, e.g. “v1”, “v2beta1”, etc.
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// Served is a flag enabling/disabling this version from being served via REST APIs
Served bool `json:"served" protobuf:"varint,2,opt,name=served"`
// Storage flags the version as storage version. There must be exactly one
// flagged as storage version.
Storage bool `json:"storage" protobuf:"varint,3,opt,name=storage"`
} }
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition // CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
...@@ -117,6 +142,14 @@ type CustomResourceDefinitionStatus struct { ...@@ -117,6 +142,14 @@ type CustomResourceDefinitionStatus struct {
// AcceptedNames are the names that are actually being used to serve discovery // AcceptedNames are the names that are actually being used to serve discovery
// They may be different than the names in spec. // They may be different than the names in spec.
AcceptedNames CustomResourceDefinitionNames `json:"acceptedNames" protobuf:"bytes,2,opt,name=acceptedNames"` AcceptedNames CustomResourceDefinitionNames `json:"acceptedNames" protobuf:"bytes,2,opt,name=acceptedNames"`
// StoredVersions are all versions of CustomResources that were ever persisted. Tracking these
// versions allows a migration path for stored versions in etcd. The field is mutable
// so the migration controller can first finish a migration to another version (i.e.
// that no old objects are left in the storage), and then remove the rest of the
// versions from this list.
// None of the versions in this list can be removed from the spec.Versions field.
StoredVersions []string `json:"storedVersions" protobuf:"bytes,3,rep,name=storedVersions"`
} }
// CustomResourceCleanupFinalizer is the name of the finalizer which will delete instances of // CustomResourceCleanupFinalizer is the name of the finalizer which will delete instances of
......
...@@ -48,6 +48,8 @@ func RegisterConversions(scheme *runtime.Scheme) error { ...@@ -48,6 +48,8 @@ func RegisterConversions(scheme *runtime.Scheme) error {
Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec, Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec,
Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomResourceDefinitionStatus, Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomResourceDefinitionStatus,
Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus, Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus,
Convert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_CustomResourceDefinitionVersion,
Convert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_CustomResourceDefinitionVersion,
Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale, Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale,
Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale, Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale,
Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus, Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus,
...@@ -220,6 +222,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes ...@@ -220,6 +222,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources)) out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
out.Versions = *(*[]apiextensions.CustomResourceDefinitionVersion)(unsafe.Pointer(&in.Versions))
return nil return nil
} }
...@@ -245,6 +248,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes ...@@ -245,6 +248,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources)) out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
out.Versions = *(*[]CustomResourceDefinitionVersion)(unsafe.Pointer(&in.Versions))
return nil return nil
} }
...@@ -258,6 +262,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomR ...@@ -258,6 +262,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomR
if err := Convert_v1beta1_CustomResourceDefinitionNames_To_apiextensions_CustomResourceDefinitionNames(&in.AcceptedNames, &out.AcceptedNames, s); err != nil { if err := Convert_v1beta1_CustomResourceDefinitionNames_To_apiextensions_CustomResourceDefinitionNames(&in.AcceptedNames, &out.AcceptedNames, s); err != nil {
return err return err
} }
out.StoredVersions = *(*[]string)(unsafe.Pointer(&in.StoredVersions))
return nil return nil
} }
...@@ -271,6 +276,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomR ...@@ -271,6 +276,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomR
if err := Convert_apiextensions_CustomResourceDefinitionNames_To_v1beta1_CustomResourceDefinitionNames(&in.AcceptedNames, &out.AcceptedNames, s); err != nil { if err := Convert_apiextensions_CustomResourceDefinitionNames_To_v1beta1_CustomResourceDefinitionNames(&in.AcceptedNames, &out.AcceptedNames, s); err != nil {
return err return err
} }
out.StoredVersions = *(*[]string)(unsafe.Pointer(&in.StoredVersions))
return nil return nil
} }
...@@ -279,6 +285,30 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou ...@@ -279,6 +285,30 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou
return autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus(in, out, s) return autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus(in, out, s)
} }
func autoConvert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_CustomResourceDefinitionVersion(in *CustomResourceDefinitionVersion, out *apiextensions.CustomResourceDefinitionVersion, s conversion.Scope) error {
out.Name = in.Name
out.Served = in.Served
out.Storage = in.Storage
return nil
}
// Convert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_CustomResourceDefinitionVersion is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_CustomResourceDefinitionVersion(in *CustomResourceDefinitionVersion, out *apiextensions.CustomResourceDefinitionVersion, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_CustomResourceDefinitionVersion(in, out, s)
}
func autoConvert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_CustomResourceDefinitionVersion(in *apiextensions.CustomResourceDefinitionVersion, out *CustomResourceDefinitionVersion, s conversion.Scope) error {
out.Name = in.Name
out.Served = in.Served
out.Storage = in.Storage
return nil
}
// Convert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_CustomResourceDefinitionVersion is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_CustomResourceDefinitionVersion(in *apiextensions.CustomResourceDefinitionVersion, out *CustomResourceDefinitionVersion, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_CustomResourceDefinitionVersion(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error { func autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error {
out.SpecReplicasPath = in.SpecReplicasPath out.SpecReplicasPath = in.SpecReplicasPath
out.StatusReplicasPath = in.StatusReplicasPath out.StatusReplicasPath = in.StatusReplicasPath
......
...@@ -150,6 +150,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -150,6 +150,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
if in.Versions != nil {
in, out := &in.Versions, &out.Versions
*out = make([]CustomResourceDefinitionVersion, len(*in))
copy(*out, *in)
}
return return
} }
...@@ -174,6 +179,11 @@ func (in *CustomResourceDefinitionStatus) DeepCopyInto(out *CustomResourceDefini ...@@ -174,6 +179,11 @@ func (in *CustomResourceDefinitionStatus) DeepCopyInto(out *CustomResourceDefini
} }
} }
in.AcceptedNames.DeepCopyInto(&out.AcceptedNames) in.AcceptedNames.DeepCopyInto(&out.AcceptedNames)
if in.StoredVersions != nil {
in, out := &in.StoredVersions, &out.StoredVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }
...@@ -188,6 +198,22 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -188,6 +198,22 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceDefinitionVersion) DeepCopyInto(out *CustomResourceDefinitionVersion) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceDefinitionVersion.
func (in *CustomResourceDefinitionVersion) DeepCopy() *CustomResourceDefinitionVersion {
if in == nil {
return nil
}
out := new(CustomResourceDefinitionVersion)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) { func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in *out = *in
if in.LabelSelectorPath != nil { if in.LabelSelectorPath != nil {
......
...@@ -45,6 +45,7 @@ func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinitio ...@@ -45,6 +45,7 @@ func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinitio
allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata")) allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateCustomResourceDefinitionSpec(&obj.Spec, field.NewPath("spec"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSpec(&obj.Spec, field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
return allErrs return allErrs
} }
...@@ -53,6 +54,34 @@ func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomRes ...@@ -53,6 +54,34 @@ func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomRes
allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata")) allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, apiextensions.IsCRDConditionTrue(oldObj, apiextensions.Established), field.NewPath("spec"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, apiextensions.IsCRDConditionTrue(oldObj, apiextensions.Established), field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
return allErrs
}
// ValidateCustomResourceDefinitionStoredVersions statically validates
func ValidateCustomResourceDefinitionStoredVersions(storedVersions []string, versions []apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path) field.ErrorList {
if len(storedVersions) == 0 {
return field.ErrorList{field.Invalid(fldPath, storedVersions, "must have at least one stored version")}
}
allErrs := field.ErrorList{}
storedVersionsMap := map[string]int{}
for i, v := range storedVersions {
storedVersionsMap[v] = i
}
for _, v := range versions {
_, ok := storedVersionsMap[v.Name]
if v.Storage && !ok {
allErrs = append(allErrs, field.Invalid(fldPath, v, "must have the storage version "+v.Name))
}
if ok {
delete(storedVersionsMap, v.Name)
}
}
for v, i := range storedVersionsMap {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, "must appear in spec.versions"))
}
return allErrs return allErrs
} }
...@@ -75,12 +104,6 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -75,12 +104,6 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot")) allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot"))
} }
if len(spec.Version) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("version"), ""))
} else if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ",")))
}
switch spec.Scope { switch spec.Scope {
case "": case "":
allErrs = append(allErrs, field.Required(fldPath.Child("scope"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("scope"), ""))
...@@ -89,6 +112,37 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -89,6 +112,37 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs = append(allErrs, field.NotSupported(fldPath.Child("scope"), spec.Scope, []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)})) allErrs = append(allErrs, field.NotSupported(fldPath.Child("scope"), spec.Scope, []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)}))
} }
storageFlagCount := 0
versionsMap := map[string]bool{}
uniqueNames := true
for i, version := range spec.Versions {
if version.Storage {
storageFlagCount++
}
if versionsMap[version.Name] {
uniqueNames = false
} else {
versionsMap[version.Name] = true
}
if errs := validationutil.IsDNS1035Label(version.Name); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
}
}
if !uniqueNames {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must contain unique version names"))
}
if storageFlagCount != 1 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must have exactly one version marked as storage version"))
}
if len(spec.Version) != 0 {
if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ",")))
}
if len(spec.Versions) >= 1 && spec.Versions[0].Name != spec.Version {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, "must match the first version in spec.versions"))
}
}
// in addition to the basic name restrictions, some names are required for spec, but not for status // in addition to the basic name restrictions, some names are required for spec, but not for status
if len(spec.Names.Plural) == 0 { if len(spec.Names.Plural) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("names", "plural"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("names", "plural"), ""))
...@@ -130,7 +184,6 @@ func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.Cus ...@@ -130,7 +184,6 @@ func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.Cus
if established { if established {
// these effect the storage and cannot be changed therefore // these effect the storage and cannot be changed therefore
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Version, oldSpec.Version, fldPath.Child("version"))...)
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope"))...) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope"))...)
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind"))...) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind"))...)
} }
......
...@@ -150,6 +150,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -150,6 +150,11 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
if in.Versions != nil {
in, out := &in.Versions, &out.Versions
*out = make([]CustomResourceDefinitionVersion, len(*in))
copy(*out, *in)
}
return return
} }
...@@ -174,6 +179,11 @@ func (in *CustomResourceDefinitionStatus) DeepCopyInto(out *CustomResourceDefini ...@@ -174,6 +179,11 @@ func (in *CustomResourceDefinitionStatus) DeepCopyInto(out *CustomResourceDefini
} }
} }
in.AcceptedNames.DeepCopyInto(&out.AcceptedNames) in.AcceptedNames.DeepCopyInto(&out.AcceptedNames)
if in.StoredVersions != nil {
in, out := &in.StoredVersions, &out.StoredVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }
...@@ -188,6 +198,22 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -188,6 +198,22 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceDefinitionVersion) DeepCopyInto(out *CustomResourceDefinitionVersion) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceDefinitionVersion.
func (in *CustomResourceDefinitionVersion) DeepCopy() *CustomResourceDefinitionVersion {
if in == nil {
return nil
}
out := new(CustomResourceDefinitionVersion)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) { func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in *out = *in
if in.LabelSelectorPath != nil { if in.LabelSelectorPath != nil {
......
...@@ -24,6 +24,7 @@ go_library( ...@@ -24,6 +24,7 @@ go_library(
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset:go_default_library",
...@@ -84,6 +85,7 @@ filegroup( ...@@ -84,6 +85,7 @@ filegroup(
name = "all-srcs", name = "all-srcs",
srcs = [ srcs = [
":package-srcs", ":package-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:all-srcs",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:all-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:all-srcs",
], ],
tags = ["automanaged"], tags = ["automanaged"],
...@@ -93,5 +95,8 @@ go_test( ...@@ -93,5 +95,8 @@ go_test(
name = "go_default_test", name = "go_default_test",
srcs = ["customresource_handler_test.go"], srcs = ["customresource_handler_test.go"],
embed = [":go_default_library"], embed = [":go_default_library"],
deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library"], deps = [
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
],
) )
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"converter.go",
"nop_converter.go",
],
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion",
visibility = ["//visibility:public"],
deps = [
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package conversion
import (
"fmt"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// NewCRDConverter returns a new CRD converter based on the conversion settings in crd object.
func NewCRDConverter(crd *apiextensions.CustomResourceDefinition) (safe, unsafe runtime.ObjectConvertor) {
validVersions := map[schema.GroupVersion]bool{}
for _, version := range crd.Spec.Versions {
validVersions[schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}] = true
}
// The only converter right now is nopConverter. More converters will be returned based on the
// CRD object when they introduced.
unsafe = &nopConverter{
clusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped,
validVersions: validVersions,
}
return &safeConverterWrapper{unsafe}, unsafe
}
// safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
type safeConverterWrapper struct {
unsafe runtime.ObjectConvertor
}
var _ runtime.ObjectConvertor = &nopConverter{}
// ConvertFieldLabel delegate the call to the unsafe converter.
func (c *safeConverterWrapper) ConvertFieldLabel(version, kind, label, value string) (string, string, error) {
return c.unsafe.ConvertFieldLabel(version, kind, label, value)
}
// Convert makes a copy of in object and then delegate the call to the unsafe converter.
func (c *safeConverterWrapper) Convert(in, out, context interface{}) error {
inObject, ok := in.(runtime.Object)
if !ok {
return fmt.Errorf("input type %T in not valid for object conversion", in)
}
return c.unsafe.Convert(inObject.DeepCopyObject(), out, context)
}
// ConvertToVersion makes a copy of in object and then delegate the call to the unsafe converter.
func (c *safeConverterWrapper) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
return c.unsafe.ConvertToVersion(in.DeepCopyObject(), target)
}
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package conversion
import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// nopConverter is a converter that only sets the apiVersion fields, but does not real conversion. It supports fields selectors.
type nopConverter struct {
clusterScoped bool
validVersions map[schema.GroupVersion]bool
}
var _ runtime.ObjectConvertor = &nopConverter{}
func (c *nopConverter) ConvertFieldLabel(version, kind, label, value string) (string, string, error) {
// We currently only support metadata.namespace and metadata.name.
switch {
case label == "metadata.name":
return label, value, nil
case !c.clusterScoped && label == "metadata.namespace":
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
}
}
func (c *nopConverter) Convert(in, out, context interface{}) error {
unstructIn, ok := in.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("input type %T in not valid for unstructured conversion", in)
}
unstructOut, ok := out.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("output type %T in not valid for unstructured conversion", out)
}
outGVK := unstructOut.GroupVersionKind()
if !c.validVersions[outGVK.GroupVersion()] {
return fmt.Errorf("request to convert CRD from an invalid group/version: %s", outGVK.String())
}
inGVK := unstructIn.GroupVersionKind()
if !c.validVersions[inGVK.GroupVersion()] {
return fmt.Errorf("request to convert CRD to an invalid group/version: %s", inGVK.String())
}
unstructOut.SetUnstructuredContent(unstructIn.UnstructuredContent())
_, err := c.ConvertToVersion(unstructOut, outGVK.GroupVersion())
if err != nil {
return err
}
return nil
}
func (c *nopConverter) convertToVersion(in runtime.Object, target runtime.GroupVersioner) error {
kind := in.GetObjectKind().GroupVersionKind()
gvk, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{kind})
if !ok {
// TODO: should this be a typed error?
return fmt.Errorf("%v is unstructured and is not suitable for converting to %q", kind, target)
}
if !c.validVersions[gvk.GroupVersion()] {
return fmt.Errorf("request to convert CRD to an invalid group/version: %s", gvk.String())
}
in.GetObjectKind().SetGroupVersionKind(gvk)
return nil
}
// ConvertToVersion converts in object to the given gvk in place and returns the same `in` object.
func (c *nopConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
var err error
// Run the converter on the list items instead of list itself
if list, ok := in.(*unstructured.UnstructuredList); ok {
err = list.EachListItem(func(item runtime.Object) error {
return c.convertToVersion(item, target)
})
}
err = c.convertToVersion(in, target)
return in, err
}
...@@ -18,6 +18,7 @@ package apiserver ...@@ -18,6 +18,7 @@ package apiserver
import ( import (
"fmt" "fmt"
"sort"
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
...@@ -28,6 +29,7 @@ import ( ...@@ -28,6 +29,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/endpoints/discovery" "k8s.io/apiserver/pkg/endpoints/discovery"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue" "k8s.io/client-go/util/workqueue"
...@@ -75,6 +77,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { ...@@ -75,6 +77,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{} apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
apiResourcesForDiscovery := []metav1.APIResource{} apiResourcesForDiscovery := []metav1.APIResource{}
versionsForDiscoveryMap := map[metav1.GroupVersion]bool{}
crds, err := c.crdLister.List(labels.Everything()) crds, err := c.crdLister.List(labels.Everything())
if err != nil { if err != nil {
...@@ -90,13 +93,29 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { ...@@ -90,13 +93,29 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
if crd.Spec.Group != version.Group { if crd.Spec.Group != version.Group {
continue continue
} }
foundGroup = true
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
GroupVersion: crd.Spec.Group + "/" + crd.Spec.Version,
Version: crd.Spec.Version,
})
if crd.Spec.Version != version.Version { foundThisVersion := false
for _, v := range crd.Spec.Versions {
if !v.Served {
continue
}
// If there is any Served version, that means the group should show up in discovery
foundGroup = true
gv := metav1.GroupVersion{Group: crd.Spec.Group, Version: v.Name}
if !versionsForDiscoveryMap[gv] {
versionsForDiscoveryMap[gv] = true
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
GroupVersion: crd.Spec.Group + "/" + v.Name,
Version: v.Name,
})
}
if v.Name == version.Version {
foundThisVersion = true
}
}
if !foundThisVersion {
continue continue
} }
foundVersion = true foundVersion = true
...@@ -144,10 +163,13 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { ...@@ -144,10 +163,13 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
return nil return nil
} }
sortGroupDiscoveryByKubeAwareVersion(apiVersionsForDiscovery)
apiGroup := metav1.APIGroup{ apiGroup := metav1.APIGroup{
Name: version.Group, Name: version.Group,
Versions: apiVersionsForDiscovery, Versions: apiVersionsForDiscovery,
// the preferred versions for a group is arbitrary since there cannot be duplicate resources // the preferred versions for a group is the first item in
// apiVersionsForDiscovery after it put in the right ordered
PreferredVersion: apiVersionsForDiscovery[0], PreferredVersion: apiVersionsForDiscovery[0],
} }
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup)) c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))
...@@ -163,6 +185,12 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { ...@@ -163,6 +185,12 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
return nil return nil
} }
func sortGroupDiscoveryByKubeAwareVersion(gd []metav1.GroupVersionForDiscovery) {
sort.Slice(gd, func(i, j int) bool {
return version.CompareKubeAwareVersionStrings(gd[i].Version, gd[j].Version) > 0
})
}
func (c *DiscoveryController) Run(stopCh <-chan struct{}) { func (c *DiscoveryController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash() defer utilruntime.HandleCrash()
defer c.queue.ShutDown() defer c.queue.ShutDown()
...@@ -207,7 +235,9 @@ func (c *DiscoveryController) processNextWorkItem() bool { ...@@ -207,7 +235,9 @@ func (c *DiscoveryController) processNextWorkItem() bool {
} }
func (c *DiscoveryController) enqueue(obj *apiextensions.CustomResourceDefinition) { func (c *DiscoveryController) enqueue(obj *apiextensions.CustomResourceDefinition) {
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: obj.Spec.Version}) for _, v := range obj.Spec.Versions {
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: v.Name})
}
} }
func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) { func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
...@@ -216,10 +246,14 @@ func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) { ...@@ -216,10 +246,14 @@ func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
c.enqueue(castObj) c.enqueue(castObj)
} }
func (c *DiscoveryController) updateCustomResourceDefinition(obj, _ interface{}) { func (c *DiscoveryController) updateCustomResourceDefinition(oldObj, newObj interface{}) {
castObj := obj.(*apiextensions.CustomResourceDefinition) castNewObj := newObj.(*apiextensions.CustomResourceDefinition)
glog.V(4).Infof("Updating customresourcedefinition %s", castObj.Name) castOldObj := oldObj.(*apiextensions.CustomResourceDefinition)
c.enqueue(castObj) glog.V(4).Infof("Updating customresourcedefinition %s", castOldObj.Name)
// Enqueue both old and new object to make sure we remove and add appropriate Versions.
// The working queue will resolve any duplicates and only changes will stay in the queue.
c.enqueue(castNewObj)
c.enqueue(castOldObj)
} }
func (c *DiscoveryController) deleteCustomResourceDefinition(obj interface{}) { func (c *DiscoveryController) deleteCustomResourceDefinition(obj interface{}) {
......
...@@ -19,7 +19,8 @@ package apiserver ...@@ -19,7 +19,8 @@ package apiserver
import ( import (
"testing" "testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
conversion "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
) )
func TestConvertFieldLabel(t *testing.T) { func TestConvertFieldLabel(t *testing.T) {
...@@ -64,10 +65,14 @@ func TestConvertFieldLabel(t *testing.T) { ...@@ -64,10 +65,14 @@ func TestConvertFieldLabel(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
c := crdObjectConverter{ crd := apiextensions.CustomResourceDefinition{}
UnstructuredObjectConverter: unstructured.UnstructuredObjectConverter{},
clusterScoped: test.clusterScoped, if test.clusterScoped {
crd.Spec.Scope = apiextensions.ClusterScoped
} else {
crd.Spec.Scope = apiextensions.NamespaceScoped
} }
_, c := conversion.NewCRDConverter(&crd)
label, value, err := c.ConvertFieldLabel("", "", test.label, "value") label, value, err := c.ConvertFieldLabel("", "", test.label, "value")
if e, a := test.expectError, err != nil; e != a { if e, a := test.expectError, err != nil; e != a {
......
...@@ -62,6 +62,15 @@ func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { ...@@ -62,6 +62,15 @@ func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
crd.Spec.Subresources = nil crd.Spec.Subresources = nil
} }
for _, v := range crd.Spec.Versions {
if v.Storage {
if !apiextensions.IsStoredVersion(crd, v.Name) {
crd.Status.StoredVersions = append(crd.Status.StoredVersions, v.Name)
}
break
}
}
} }
// PrepareForUpdate clears fields that are not allowed to be set by end users on update. // PrepareForUpdate clears fields that are not allowed to be set by end users on update.
...@@ -90,6 +99,15 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { ...@@ -90,6 +99,15 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newCRD.Spec.Subresources = nil newCRD.Spec.Subresources = nil
oldCRD.Spec.Subresources = nil oldCRD.Spec.Subresources = nil
} }
for _, v := range newCRD.Spec.Versions {
if v.Storage {
if !apiextensions.IsStoredVersion(newCRD, v.Name) {
newCRD.Status.StoredVersions = append(newCRD.Status.StoredVersions, v.Name)
}
break
}
}
} }
// Validate validates a new CustomResourceDefinition. // Validate validates a new CustomResourceDefinition.
......
...@@ -13,6 +13,7 @@ go_test( ...@@ -13,6 +13,7 @@ go_test(
"registration_test.go", "registration_test.go",
"subresources_test.go", "subresources_test.go",
"validation_test.go", "validation_test.go",
"versioning_test.go",
"yaml_test.go", "yaml_test.go",
], ],
tags = ["integration"], tags = ["integration"],
......
...@@ -68,6 +68,42 @@ func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unst ...@@ -68,6 +68,42 @@ func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unst
return createdInstance, nil return createdInstance, nil
} }
func instantiateVersionedCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client dynamic.ResourceInterface, definition *apiextensionsv1beta1.CustomResourceDefinition, version string) (*unstructured.Unstructured, error) {
createdInstance, err := client.Create(instanceToCreate)
if err != nil {
t.Logf("%#v", createdInstance)
return nil, err
}
createdObjectMeta, err := meta.Accessor(createdInstance)
if err != nil {
t.Fatal(err)
}
// it should have a UUID
if len(createdObjectMeta.GetUID()) == 0 {
t.Errorf("missing uuid: %#v", createdInstance)
}
createdTypeMeta, err := meta.TypeAccessor(createdInstance)
if err != nil {
t.Fatal(err)
}
if e, a := definition.Spec.Group+"/"+version, createdTypeMeta.GetAPIVersion(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := definition.Spec.Names.Kind, createdTypeMeta.GetKind(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
return createdInstance, nil
}
func NewNamespacedCustomResourceVersionedClient(ns string, client dynamic.Interface, crd *apiextensionsv1beta1.CustomResourceDefinition, version string) dynamic.ResourceInterface {
gvr := schema.GroupVersionResource{Group: crd.Spec.Group, Version: version, Resource: crd.Spec.Names.Plural}
if crd.Spec.Scope != apiextensionsv1beta1.ClusterScoped {
return client.Resource(gvr).Namespace(ns)
}
return client.Resource(gvr)
}
func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, crd *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface { func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, crd *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
gvr := schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Spec.Names.Plural} gvr := schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Spec.Names.Plural}
......
...@@ -100,6 +100,62 @@ func NewNoxuInstance(namespace, name string) *unstructured.Unstructured { ...@@ -100,6 +100,62 @@ func NewNoxuInstance(namespace, name string) *unstructured.Unstructured {
} }
} }
func NewMultipleVersionNoxuCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Version: "v1beta1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "noxus",
Singular: "nonenglishnoxu",
Kind: "WishIHadChosenNoxu",
ShortNames: []string{"foo", "bar", "abc", "def"},
ListKind: "NoxuItemList",
Categories: []string{"all"},
},
Scope: scope,
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: false,
},
{
Name: "v1beta2",
Served: true,
Storage: true,
},
{
Name: "v0",
Served: false,
Storage: false,
},
},
},
}
}
func NewVersionedNoxuInstance(namespace, name, version string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/" + version,
"kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
"content": map[string]interface{}{
"key": "value",
},
"num": map[string]interface{}{
"num1": noxuInstanceNum,
"num2": 1000000,
},
},
}
}
func NewNoxu2CustomResourceDefinition(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition { func NewNoxu2CustomResourceDefinition(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{ return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "noxus2.mygroup.example.com"}, ObjectMeta: metav1.ObjectMeta{Name: "noxus2.mygroup.example.com"},
......
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package integration
import (
"reflect"
"testing"
"time"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
)
func TestVersionedNamspacedScopedCRD(t *testing.T) {
stopCh, apiExtensionClient, dynamicClient, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.NamespaceScoped)
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
testSimpleVersionedCRUD(t, ns, noxuDefinition, dynamicClient)
}
func TestVersionedClusterScopedCRD(t *testing.T) {
stopCh, apiExtensionClient, dynamicClient, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.ClusterScoped)
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := ""
testSimpleVersionedCRUD(t, ns, noxuDefinition, dynamicClient)
}
func TestStoragedVersionInNamespacedCRDStatus(t *testing.T) {
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.NamespaceScoped)
ns := "not-the-default"
testStoragedVersionInCRDStatus(t, ns, noxuDefinition)
}
func TestStoragedVersionInClusterScopedCRDStatus(t *testing.T) {
noxuDefinition := testserver.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.ClusterScoped)
ns := ""
testStoragedVersionInCRDStatus(t, ns, noxuDefinition)
}
func testStoragedVersionInCRDStatus(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition) {
versionsV1Beta1Storage := []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: true,
},
{
Name: "v1beta2",
Served: true,
Storage: false,
},
}
versionsV1Beta2Storage := []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: false,
},
{
Name: "v1beta2",
Served: true,
Storage: true,
},
}
stopCh, apiExtensionClient, dynamicClient, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition.Spec.Versions = versionsV1Beta1Storage
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
// The storage version list should be initilized to storage version
crd, err := testserver.GetCustomResourceDefinition(noxuDefinition, apiExtensionClient)
if err != nil {
t.Fatal(err)
}
if e, a := []string{"v1beta1"}, crd.Status.StoredVersions; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
// Changing CRD storage version should be reflected immediately
crd.Spec.Versions = versionsV1Beta2Storage
_, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
if err != nil {
t.Fatal(err)
}
crd, err = testserver.GetCustomResourceDefinition(noxuDefinition, apiExtensionClient)
if err != nil {
t.Fatal(err)
}
if e, a := []string{"v1beta1", "v1beta2"}, crd.Status.StoredVersions; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
err = testserver.DeleteCustomResourceDefinition(crd, apiExtensionClient)
if err != nil {
t.Fatal(err)
}
}
func testSimpleVersionedCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, dynamicClient dynamic.Interface) {
noxuResourceClients := map[string]dynamic.ResourceInterface{}
noxuWatchs := map[string]watch.Interface{}
disbaledVersions := map[string]bool{}
for _, v := range noxuDefinition.Spec.Versions {
disbaledVersions[v.Name] = !v.Served
}
for _, v := range noxuDefinition.Spec.Versions {
noxuResourceClients[v.Name] = NewNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
noxuWatch, err := noxuResourceClients[v.Name].Watch(metav1.ListOptions{})
if disbaledVersions[v.Name] {
if err == nil {
t.Errorf("expected the watch creation fail for disabled version %s", v.Name)
}
} else {
if err != nil {
t.Fatal(err)
}
noxuWatchs[v.Name] = noxuWatch
}
}
defer func() {
for _, w := range noxuWatchs {
w.Stop()
}
}()
for version, noxuResourceClient := range noxuResourceClients {
createdNoxuInstance, err := instantiateVersionedCustomResource(t, testserver.NewVersionedNoxuInstance(ns, "foo", version), noxuResourceClient, noxuDefinition, version)
if disbaledVersions[version] {
if err == nil {
t.Errorf("expected the CR creation fail for disabled version %s", version)
}
continue
}
if err != nil {
t.Fatalf("unable to create noxu Instance:%v", err)
}
if e, a := noxuDefinition.Spec.Group+"/"+version, createdNoxuInstance.GetAPIVersion(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
for watchVersion, noxuWatch := range noxuWatchs {
select {
case watchEvent := <-noxuWatch.ResultChan():
if e, a := watch.Added, watchEvent.Type; e != a {
t.Errorf("expected %v, got %v", e, a)
break
}
createdObjectMeta, err := meta.Accessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
// it should have a UUID
if len(createdObjectMeta.GetUID()) == 0 {
t.Errorf("missing uuid: %#v", watchEvent.Object)
}
if e, a := ns, createdObjectMeta.GetNamespace(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
createdTypeMeta, err := meta.TypeAccessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
if e, a := noxuDefinition.Spec.Group+"/"+watchVersion, createdTypeMeta.GetAPIVersion(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := noxuDefinition.Spec.Names.Kind, createdTypeMeta.GetKind(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
case <-time.After(5 * time.Second):
t.Errorf("missing watch event")
}
}
// Check get for all versions
for version2, noxuResourceClient2 := range noxuResourceClients {
// Get test
gottenNoxuInstance, err := noxuResourceClient2.Get("foo", metav1.GetOptions{})
if disbaledVersions[version2] {
if err == nil {
t.Errorf("expected the get operation fail for disabled version %s", version2)
}
} else {
if err != nil {
t.Fatal(err)
}
if e, a := version2, gottenNoxuInstance.GroupVersionKind().Version; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
// List test
listWithItem, err := noxuResourceClient2.List(metav1.ListOptions{})
if disbaledVersions[version2] {
if err == nil {
t.Errorf("expected the list operation fail for disabled version %s", version2)
}
} else {
if err != nil {
t.Fatal(err)
}
if e, a := 1, len(listWithItem.Items); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := version2, listWithItem.GroupVersionKind().Version; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := version2, listWithItem.Items[0].GroupVersionKind().Version; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
}
// Delete test
if err := noxuResourceClient.Delete("foo", metav1.NewDeleteOptions(0)); err != nil {
t.Fatal(err)
}
listWithoutItem, err := noxuResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := 0, len(listWithoutItem.Items); e != a {
t.Errorf("expected %v, got %v", e, a)
}
for _, noxuWatch := range noxuWatchs {
select {
case watchEvent := <-noxuWatch.ResultChan():
if e, a := watch.Deleted, watchEvent.Type; e != a {
t.Errorf("expected %v, got %v", e, a)
break
}
deletedObjectMeta, err := meta.Accessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
// it should have a UUID
createdObjectMeta, err := meta.Accessor(createdNoxuInstance)
if err != nil {
t.Fatal(err)
}
if e, a := createdObjectMeta.GetUID(), deletedObjectMeta.GetUID(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
case <-time.After(5 * time.Second):
t.Errorf("missing watch event")
}
}
// Delete test
if err := noxuResourceClient.DeleteCollection(metav1.NewDeleteOptions(0), metav1.ListOptions{}); err != nil {
t.Fatal(err)
}
}
}
...@@ -22,6 +22,7 @@ go_library( ...@@ -22,6 +22,7 @@ go_library(
srcs = ["versioning.go"], srcs = ["versioning.go"],
importpath = "k8s.io/apimachinery/pkg/runtime/serializer/versioning", importpath = "k8s.io/apimachinery/pkg/runtime/serializer/versioning",
deps = [ deps = [
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
], ],
......
...@@ -19,6 +19,7 @@ package versioning ...@@ -19,6 +19,7 @@ package versioning
import ( import (
"io" "io"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
) )
...@@ -170,17 +171,22 @@ func (c *codec) Encode(obj runtime.Object, w io.Writer) error { ...@@ -170,17 +171,22 @@ func (c *codec) Encode(obj runtime.Object, w io.Writer) error {
case *runtime.Unknown: case *runtime.Unknown:
return c.encoder.Encode(obj, w) return c.encoder.Encode(obj, w)
case runtime.Unstructured: case runtime.Unstructured:
// avoid conversion roundtrip if GVK is the right one already or is empty (yes, this is a hack, but the old behaviour we rely on in kubectl) // An unstructured list can contain objects of multiple group version kinds. don't short-circuit just
objGVK := obj.GetObjectKind().GroupVersionKind() // because the top-level type matches our desired destination type. actually send the object to the converter
if len(objGVK.Version) == 0 { // to give it a chance to convert the list items if needed.
return c.encoder.Encode(obj, w) if _, ok := obj.(*unstructured.UnstructuredList); !ok {
} // avoid conversion roundtrip if GVK is the right one already or is empty (yes, this is a hack, but the old behaviour we rely on in kubectl)
targetGVK, ok := c.encodeVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{objGVK}) objGVK := obj.GetObjectKind().GroupVersionKind()
if !ok { if len(objGVK.Version) == 0 {
return runtime.NewNotRegisteredGVKErrForTarget(objGVK, c.encodeVersion) return c.encoder.Encode(obj, w)
} }
if targetGVK == objGVK { targetGVK, ok := c.encodeVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{objGVK})
return c.encoder.Encode(obj, w) if !ok {
return runtime.NewNotRegisteredGVKErrForTarget(objGVK, c.encodeVersion)
}
if targetGVK == objGVK {
return c.encoder.Encode(obj, w)
}
} }
} }
......
...@@ -279,6 +279,10 @@ ...@@ -279,6 +279,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1", "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment