Add generated file

This PR adds generated files under pkg/client and vendor folder.
This commit is contained in:
xing-yang
2018-07-12 10:55:15 -07:00
parent 36b1de0341
commit e213d1890d
17729 changed files with 5090889 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDFTCCAf2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRsb2Nh
bGhvc3RAMTUxNTQ2MjIwNjAgFw0xODAxMDkwMTQzMjZaGA8yMTE4MDEwOTAxNDMy
NlowHzEdMBsGA1UEAwwUbG9jYWxob3N0QDE1MTU0NjIyMDYwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC2hIORzonehlNadYyI30v1Jj8lhhABuiWiTSkl
KCLqZjwBfWfSC4w02zxi2SAH9ju20XCJrUauwPq1qXCp/CqXC/rVgZrzluDlpJpe
gF9AilQvGOxhrZhV4kqpOjGVE78uOmpfxiOyNermoJ0OVE8ugh3s/LLTNK/qmCAX
uEYTQccAvNEiPX3XPBCiaFlSCkUNS0zp12mJNP43+KF9y0CbtYs1gXKHmmJVSpjR
YmcuJJUfHxNrV2YR3ek6O4IIJFIlnLxgpjRBseBPkTenAT3S2YY9MyQkkBrRSPBa
vLM24al3KDvXYikYe3WpxeYNHGNcHIgR+hKlRTQ5VrWlfx9dAgMBAAGjWjBYMA4G
A1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTAD
AQH/MCAGA1UdEQQZMBeCCWxvY2FsaG9zdIcEfwAAAYcEfwAAATANBgkqhkiG9w0B
AQsFAAOCAQEAFhW8cVTraHPNsE+Jo0ZvcE2ic8lEzeOhWI2O/fpkrUJS5LptPKHS
nTK+CPxA0zhIS/vlJznIabeddXwtq7Xb5SwlJMHYMnHD6f5qwpD22D2dxJJa5sma
3yrK/4CutuEae08qqSeakfgCjcHLL9p7FZWxujkV9/5CEH5lFWYLGumyIoS46Svf
nSfDFKTrOj8P60ncCoWcSpMbdVQBDuKlIZuBMmz9CguC1CtuQWPDUmOGJuPs/+So
yusHbBfj+ATUWDYTg1lLjOIOSJpHGUQkvS+8Bo47SThD/b4w2i6VC72ldxtBuxGf
L7+jALMhMhiQD+Q4qsNuyvvNQLoYcTTFTw==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtoSDkc6J3oZTWnWMiN9L9SY/JYYQAbolok0pJSgi6mY8AX1n
0guMNNs8YtkgB/Y7ttFwia1GrsD6talwqfwqlwv61YGa85bg5aSaXoBfQIpULxjs
Ya2YVeJKqToxlRO/LjpqX8YjsjXq5qCdDlRPLoId7Pyy0zSv6pggF7hGE0HHALzR
Ij191zwQomhZUgpFDUtM6ddpiTT+N/ihfctAm7WLNYFyh5piVUqY0WJnLiSVHx8T
a1dmEd3pOjuCCCRSJZy8YKY0QbHgT5E3pwE90tmGPTMkJJAa0UjwWryzNuGpdyg7
12IpGHt1qcXmDRxjXByIEfoSpUU0OVa1pX8fXQIDAQABAoIBAERy2ezaqnXbpnLs
VrIWHCRqHZBzAJnFN8vwaBfZP47snGBqqX7qecBw3+qqRwr1W1uqnCvl4fYzxVJP
o0L8oPRYt89OddAYq2s0GfiK6C4KMpwfGrdfJRxAa4OfoWypJS+vFKmqY0S4V8n6
Pixbjf6BKbvw4Re4UKkIODDtGMqrZFVKcFe8LCnd3D+7jvt0M/WjEhrepWxscJh3
aHgDzsLzCv1DNjgZfoRZubkK3bdndMaL6NhaKNBz6S7CT9XmZsJaWkmBXs9zOoyr
0hKP0A11cm6a7LsmxX5h4uaQLh66KHUPbV4KjKgKiGkSS9cnZoXHFZLOplOfozje
1DKitAECgYEA2eWiRNByNIqqRPvBtD8ydavOLk6iLlLt+LkCpGupgELs53WS5fTT
TxbyVq+897qeW2Klir7jZFWG3Q+EaBATxMYON+jb7QnIz8gX9lh1PpUlo88BiQzO
hAIx2uV19KM0ftXYVTSAUh1N2cgoOWGUWLaeMPdxPOlJwvM25hSfp90CgYEA1m8W
vWBO8X5LXM9g+fO1TFSlTnUJW1gWrnOw4VmU2+DbqNmtefpVrqDa5Iw2+mU+EBgA
d3wdAHARXpc2MGcIRnRbHn+gXJVHA+gA7H9LSZ4Yi0qJZbNVAgRySs2iBYUcunsR
AXkS7sPGQinfnjKh6vhYVErh5jA+cvS8CXZtnYECgYBmh61hYAw9OPqB100AebRO
tncgRxP9ZDxiCvx5TcfGeLds+mATIK7FynBh5fOvRfr52WM39DafobcCEiklplsG
/oL2P/YshaweSXMtEdapihjaCbAZQxNx/m5jKBHm+VzcSdev0DKJcQyO66Yxyf65
98RcGjMIjGWO/E7a2N1/aQKBgCPrY+HBGjg1saYQTuxPuJTasP4deL3GWbZLRtvY
x6i1V9ZG8Fo4ZtXjuAcEvcjf4K+NdbaOIcWLAD3aEoe1GpvCrejD9DbOAqFS4aS8
Bf6E7xOWHsHccmbuG78QBw3pqFBMgSLABz3bqYA3x2+Wh6z2gMVN7d1DQ5K6EC19
mwsBAoGBAKZBgqRHRq1Ch3SWb5Q+SgUvNyQ+PAIwCve0vA4mMIK6EGqU/8wbU01B
5/UkCfT+ovDeDuyeaZbTWzwUC4Mrg4C9rThrK5WLc43Dig6G1HhfjdLA+gdKFOjh
FpocOI2FEwbmj5Mka6n3TSFI8c55ubYdyXQu92DoFt4dTOJStUn2
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,944 @@
/*
Copyright 2017 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 (
"fmt"
"reflect"
"sort"
"testing"
"time"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
)
func TestServerUp(t *testing.T) {
tearDown, _, _, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
}
func TestNamespaceScopedCRUD(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
testSimpleCRUD(t, ns, noxuDefinition, dynamicClient)
testFieldSelector(t, ns, noxuDefinition, dynamicClient)
}
func TestClusterScopedCRUD(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := ""
testSimpleCRUD(t, ns, noxuDefinition, dynamicClient)
testFieldSelector(t, ns, noxuDefinition, dynamicClient)
}
func testSimpleCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, dynamicClient dynamic.Interface) {
noxuResourceClients := map[string]dynamic.ResourceInterface{}
noxuWatchs := map[string]watch.Interface{}
disabledVersions := map[string]bool{}
for _, v := range noxuDefinition.Spec.Versions {
disabledVersions[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 disabledVersions[v.Name] {
if !errors.IsNotFound(err) {
t.Errorf("expected the watch operation fail with NotFound for disabled version %s, got error: %v", v.Name, err)
}
} 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, fixtures.NewVersionedNoxuInstance(ns, "foo", version), noxuResourceClient, noxuDefinition, version)
if disabledVersions[version] {
if !errors.IsNotFound(err) {
t.Errorf("expected the CR creation fail with NotFound for disabled version %s, got error: %v", version, err)
}
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 disabledVersions[version2] {
if !errors.IsNotFound(err) {
t.Errorf("expected the get operation fail with NotFound for disabled version %s, got error: %v", version2, err)
}
} 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 disabledVersions[version2] {
if !errors.IsNotFound(err) {
t.Errorf("expected the list operation fail with NotFound for disabled version %s, got error: %v", version2, err)
}
} 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)
}
}
}
// Update test
for version2, noxuResourceClient2 := range noxuResourceClients {
var gottenNoxuInstance *unstructured.Unstructured
if disabledVersions[version2] {
gottenNoxuInstance = &unstructured.Unstructured{}
gottenNoxuInstance.SetName("foo")
} else {
gottenNoxuInstance, err = noxuResourceClient2.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
}
gottenNoxuInstance.Object["updated"] = version2
updatedNoxuInstance, err := noxuResourceClient2.Update(gottenNoxuInstance)
if disabledVersions[version2] {
if !errors.IsNotFound(err) {
t.Errorf("expected the update operation fail with NotFound for disabled version %s, got error: %v", version2, err)
}
} else {
if err != nil {
t.Fatal(err)
}
if updated, ok := updatedNoxuInstance.Object["updated"]; !ok {
t.Errorf("expected string 'updated' field")
} else if updated, ok := updated.(string); !ok || updated != version2 {
t.Errorf("expected string 'updated' field to equal %q, got %q of type %T", version2, updated, updated)
}
if e, a := version2, updatedNoxuInstance.GroupVersionKind().Version; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
for _, noxuWatch := range noxuWatchs {
select {
case watchEvent := <-noxuWatch.ResultChan():
eventMetadata, err := meta.Accessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
if watchEvent.Type != watch.Modified {
t.Errorf("expected modified event, got %v", watchEvent.Type)
break
}
// it should have a UUID
createdMetadata, err := meta.Accessor(createdNoxuInstance)
if err != nil {
t.Fatal(err)
}
if e, a := createdMetadata.GetUID(), eventMetadata.GetUID(); e != a {
t.Errorf("expected equal UID for (expected) %v, and (actual) %v", createdNoxuInstance, watchEvent.Object)
}
case <-time.After(5 * time.Second):
t.Errorf("missing watch event")
}
}
}
}
// 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():
eventMetadata, err := meta.Accessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
if watchEvent.Type != watch.Deleted {
t.Errorf("expected delete event, got %v", watchEvent.Type)
break
}
// it should have a UUID
createdMetadata, err := meta.Accessor(createdNoxuInstance)
if err != nil {
t.Fatal(err)
}
if e, a := createdMetadata.GetUID(), eventMetadata.GetUID(); e != a {
t.Errorf("expected equal UID for (expected) %v, and (actual) %v", createdNoxuInstance, watchEvent.Object)
}
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)
}
}
}
func testFieldSelector(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, dynamicClient dynamic.Interface) {
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
initialList, err := noxuResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := 0, len(initialList.Items); e != a {
t.Errorf("expected %v, got %v", e, a)
}
initialListTypeMeta, err := meta.TypeAccessor(initialList)
if err != nil {
t.Fatal(err)
}
if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Versions[0].Name, initialListTypeMeta.GetAPIVersion(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := noxuDefinition.Spec.Names.ListKind, initialListTypeMeta.GetKind(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
initialListListMeta, err := meta.ListAccessor(initialList)
if err != nil {
t.Fatal(err)
}
noxuWatch, err := noxuResourceClient.Watch(
metav1.ListOptions{
ResourceVersion: initialListListMeta.GetResourceVersion(),
FieldSelector: "metadata.name=foo",
},
)
if err != nil {
t.Fatal(err)
}
defer noxuWatch.Stop()
_, err = instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, "bar"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu Instance:%v", err)
}
createdNoxuInstanceFoo, err := instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu Instance:%v", err)
}
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)
}
if e, a := "foo", createdObjectMeta.GetName(); 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+"/"+noxuDefinition.Spec.Versions[0].Name, 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")
}
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := createdNoxuInstanceFoo, gottenNoxuInstance; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
listWithItem, err := noxuResourceClient.List(metav1.ListOptions{FieldSelector: "metadata.name=foo"})
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 := *createdNoxuInstanceFoo, listWithItem.Items[0]; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
if err := noxuResourceClient.Delete("bar", nil); err != nil {
t.Fatal(err)
}
if err := noxuResourceClient.Delete("foo", nil); 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)
}
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(createdNoxuInstanceFoo)
if err != nil {
t.Fatal(err)
}
if e, a := createdObjectMeta.GetUID(), deletedObjectMeta.GetUID(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := ns, createdObjectMeta.GetNamespace(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := "foo", createdObjectMeta.GetName(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
case <-time.After(5 * time.Second):
t.Errorf("missing watch event")
}
}
func TestDiscovery(t *testing.T) {
group := "mygroup.example.com"
version := "v1beta1"
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
scope := apiextensionsv1beta1.NamespaceScoped
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(scope)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
// check whether it shows up in discovery properly
resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
if err != nil {
t.Fatal(err)
}
if len(resources.APIResources) != 1 {
t.Fatalf("Expected exactly the resource \"noxus\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources)
}
r := resources.APIResources[0]
if r.Name != "noxus" {
t.Fatalf("Expected exactly the resource \"noxus\" in group version %v/%v via discovery, got: %v", group, version, r.Name)
}
if r.Kind != "WishIHadChosenNoxu" {
t.Fatalf("Expected exactly the kind \"WishIHadChosenNoxu\" in group version %v/%v via discovery, got: %v", group, version, r.Kind)
}
s := []string{"foo", "bar", "abc", "def"}
if !reflect.DeepEqual(r.ShortNames, s) {
t.Fatalf("Expected exactly the shortnames `foo, bar, abc, def` in group version %v/%v via discovery, got: %v", group, version, r.ShortNames)
}
sort.Strings(r.Verbs)
expectedVerbs := []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}
if !reflect.DeepEqual([]string(r.Verbs), expectedVerbs) {
t.Fatalf("Unexpected verbs for resource \"noxus\" in group version %v/%v via discovery: expected=%v got=%v", group, version, expectedVerbs, r.Verbs)
}
if !reflect.DeepEqual(r.Categories, []string{"all"}) {
t.Fatalf("Expected exactly the category \"all\" in group version %v/%v via discovery, got: %v", group, version, r.Categories)
}
}
func TestNoNamespaceReject(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := ""
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
initialList, err := noxuResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := 0, len(initialList.Items); e != a {
t.Errorf("expected %v, got %v", e, a)
}
initialListTypeMeta, err := meta.TypeAccessor(initialList)
if err != nil {
t.Fatal(err)
}
if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Version, initialListTypeMeta.GetAPIVersion(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := noxuDefinition.Spec.Names.ListKind, initialListTypeMeta.GetKind(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
createdNoxuInstance, err := instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err == nil {
t.Fatalf("unexpected non-error: an empty namespace may not be set during creation while creating noxu instance: %v ", createdNoxuInstance)
}
}
func TestSameNameDiffNamespace(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns1 := "namespace-1"
testSimpleCRUD(t, ns1, noxuDefinition, dynamicClient)
ns2 := "namespace-2"
testSimpleCRUD(t, ns2, noxuDefinition, dynamicClient)
}
func TestSelfLink(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
// namespace scoped
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
noxuInstanceToCreate := fixtures.NewNoxuInstance(ns, "foo")
createdNoxuInstance, err := noxuNamespacedResourceClient.Create(noxuInstanceToCreate)
if err != nil {
t.Fatal(err)
}
if e, a := "/apis/mygroup.example.com/v1beta1/namespaces/not-the-default/noxus/foo", createdNoxuInstance.GetSelfLink(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
// cluster scoped
curletDefinition := fixtures.NewCurletCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
curletDefinition, err = fixtures.CreateNewCustomResourceDefinition(curletDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
curletResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, curletDefinition)
curletInstanceToCreate := fixtures.NewCurletInstance(ns, "foo")
createdCurletInstance, err := curletResourceClient.Create(curletInstanceToCreate)
if err != nil {
t.Fatal(err)
}
if e, a := "/apis/mygroup.example.com/v1beta1/curlets/foo", createdCurletInstance.GetSelfLink(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
}
func TestPreserveInt(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
noxuInstanceToCreate := fixtures.NewNoxuInstance(ns, "foo")
createdNoxuInstance, err := noxuNamespacedResourceClient.Create(noxuInstanceToCreate)
if err != nil {
t.Fatal(err)
}
originalJSON, err := runtime.Encode(unstructured.UnstructuredJSONScheme, createdNoxuInstance)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
gottenNoxuInstance, err := runtime.Decode(unstructured.UnstructuredJSONScheme, originalJSON)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Check if int is preserved.
unstructuredObj := gottenNoxuInstance.(*unstructured.Unstructured).Object
num := unstructuredObj["num"].(map[string]interface{})
num1 := num["num1"].(int64)
num2 := num["num2"].(int64)
if num1 != 9223372036854775807 || num2 != 1000000 {
t.Errorf("Expected %v, got %v, %v", `9223372036854775807, 1000000`, num1, num2)
}
}
func TestPatch(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
noxuInstanceToCreate := fixtures.NewNoxuInstance(ns, "foo")
createdNoxuInstance, err := noxuNamespacedResourceClient.Create(noxuInstanceToCreate)
if err != nil {
t.Fatal(err)
}
patch := []byte(`{"num": {"num2":999}}`)
createdNoxuInstance, err = noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// a patch with no change
createdNoxuInstance, err = noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// an empty patch
createdNoxuInstance, err = noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
originalJSON, err := runtime.Encode(unstructured.UnstructuredJSONScheme, createdNoxuInstance)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
gottenNoxuInstance, err := runtime.Decode(unstructured.UnstructuredJSONScheme, originalJSON)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Check if int is preserved.
unstructuredObj := gottenNoxuInstance.(*unstructured.Unstructured).Object
num := unstructuredObj["num"].(map[string]interface{})
num1 := num["num1"].(int64)
num2 := num["num2"].(int64)
if num1 != 9223372036854775807 || num2 != 999 {
t.Errorf("Expected %v, got %v, %v", `9223372036854775807, 999`, num1, num2)
}
}
func TestCrossNamespaceListWatch(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := ""
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
initialList, err := noxuResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := 0, len(initialList.Items); e != a {
t.Errorf("expected %v, got %v", e, a)
}
initialListListMeta, err := meta.ListAccessor(initialList)
if err != nil {
t.Fatal(err)
}
noxuWatch, err := noxuResourceClient.Watch(metav1.ListOptions{ResourceVersion: initialListListMeta.GetResourceVersion()})
if err != nil {
t.Fatal(err)
}
defer noxuWatch.Stop()
instances := make(map[string]*unstructured.Unstructured)
ns1 := "namespace-1"
noxuNamespacedResourceClient1 := newNamespacedCustomResourceClient(ns1, dynamicClient, noxuDefinition)
instances[ns1] = createInstanceWithNamespaceHelper(t, ns1, "foo1", noxuNamespacedResourceClient1, noxuDefinition)
noxuNamespacesWatch1, err := noxuNamespacedResourceClient1.Watch(metav1.ListOptions{ResourceVersion: initialListListMeta.GetResourceVersion()})
defer noxuNamespacesWatch1.Stop()
ns2 := "namespace-2"
noxuNamespacedResourceClient2 := newNamespacedCustomResourceClient(ns2, dynamicClient, noxuDefinition)
instances[ns2] = createInstanceWithNamespaceHelper(t, ns2, "foo2", noxuNamespacedResourceClient2, noxuDefinition)
noxuNamespacesWatch2, err := noxuNamespacedResourceClient2.Watch(metav1.ListOptions{ResourceVersion: initialListListMeta.GetResourceVersion()})
defer noxuNamespacesWatch2.Stop()
createdList, err := noxuResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := 2, len(createdList.Items); e != a {
t.Errorf("expected %v, got %v", e, a)
}
for _, a := range createdList.Items {
if e := instances[a.GetNamespace()]; !reflect.DeepEqual(e, &a) {
t.Errorf("expected %v, got %v", e, a)
}
}
addEvents := 0
for addEvents < 2 {
select {
case watchEvent := <-noxuWatch.ResultChan():
if e, a := watch.Added, watchEvent.Type; e != a {
t.Fatalf("expected %v, got %v", e, a)
}
createdObjectMeta, err := meta.Accessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
if len(createdObjectMeta.GetUID()) == 0 {
t.Errorf("missing uuid: %#v", watchEvent.Object)
}
createdTypeMeta, err := meta.TypeAccessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Version, 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)
}
delete(instances, createdObjectMeta.GetNamespace())
addEvents++
case <-time.After(5 * time.Second):
t.Fatalf("missing watch event")
}
}
if e, a := 0, len(instances); e != a {
t.Errorf("expected %v, got %v", e, a)
}
checkNamespacesWatchHelper(t, ns1, noxuNamespacesWatch1)
checkNamespacesWatchHelper(t, ns2, noxuNamespacesWatch2)
}
func createInstanceWithNamespaceHelper(t *testing.T, ns string, name string, noxuNamespacedResourceClient dynamic.ResourceInterface, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition) *unstructured.Unstructured {
createdInstance, err := instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, name), noxuNamespacedResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu Instance:%v", err)
}
return createdInstance
}
func checkNamespacesWatchHelper(t *testing.T, ns string, namespacedwatch watch.Interface) {
namespacedAddEvent := 0
for namespacedAddEvent < 2 {
select {
case watchEvent := <-namespacedwatch.ResultChan():
// Check that the namespaced watch only has one result
if namespacedAddEvent > 0 {
t.Fatalf("extra watch event")
}
if e, a := watch.Added, watchEvent.Type; e != a {
t.Fatalf("expected %v, got %v", e, a)
}
createdObjectMeta, err := meta.Accessor(watchEvent.Object)
if err != nil {
t.Fatal(err)
}
if e, a := ns, createdObjectMeta.GetNamespace(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
case <-time.After(5 * time.Second):
if namespacedAddEvent != 1 {
t.Fatalf("missing watch event")
}
}
namespacedAddEvent++
}
}
func TestNameConflict(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxu2Definition := fixtures.NewNoxu2CustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
_, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(noxu2Definition)
if err != nil {
t.Fatal(err)
}
// A NameConflict occurs
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
crd, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(noxu2Definition.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
for _, condition := range crd.Status.Conditions {
if condition.Type == apiextensionsv1beta1.NamesAccepted && condition.Status == apiextensionsv1beta1.ConditionFalse {
return true, nil
}
}
return false, nil
})
if err != nil {
t.Fatal(err)
}
err = fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient)
if err != nil {
t.Fatal(err)
}
// Names are now accepted
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
crd, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(noxu2Definition.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
for _, condition := range crd.Status.Conditions {
if condition.Type == apiextensionsv1beta1.NamesAccepted && condition.Status == apiextensionsv1beta1.ConditionTrue {
return true, nil
}
}
return false, nil
})
if err != nil {
t.Fatal(err)
}
}
func TestStatusGetAndPatch(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
// make sure we don't get 405 Method Not Allowed from Getting CRD/status subresource
result := &apiextensionsv1beta1.CustomResourceDefinition{}
err = apiExtensionClient.ApiextensionsV1beta1().RESTClient().Get().
Resource("customresourcedefinitions").
Name(noxuDefinition.Name).
SubResource("status").
Do().
Into(result)
if err != nil {
t.Fatal(err)
}
// make sure we don't get 405 Method Not Allowed from Patching CRD/status subresource
_, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().
Patch(noxuDefinition.Name, types.StrategicMergePatchType,
[]byte(fmt.Sprintf(`{"labels":{"test-label":"dummy"}}`)),
"status")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,165 @@
/*
Copyright 2017 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 (
"testing"
"time"
"github.com/stretchr/testify/require"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
)
func TestFinalization(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
require.NoError(t, err)
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
require.NoError(t, err)
ns := "not-the-default"
name := "foo123"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
instance := fixtures.NewNoxuInstance(ns, name)
instance.SetFinalizers([]string{"noxu.example.com/finalizer"})
createdNoxuInstance, err := instantiateCustomResource(t, instance, noxuResourceClient, noxuDefinition)
require.NoError(t, err)
uid := createdNoxuInstance.GetUID()
err = noxuResourceClient.Delete(name, &metav1.DeleteOptions{
Preconditions: &metav1.Preconditions{
UID: &uid,
},
})
require.NoError(t, err)
// Deleting something with a finalizer sets deletion timestamp to a not-nil value but does not
// remove the object from the API server. Here we read it to confirm this.
gottenNoxuInstance, err := noxuResourceClient.Get(name, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, gottenNoxuInstance.GetDeletionTimestamp())
// Trying to delete it again to confirm it will not remove the object because finalizer is still there.
err = noxuResourceClient.Delete(name, &metav1.DeleteOptions{
Preconditions: &metav1.Preconditions{
UID: &uid,
},
})
require.NoError(t, err)
// Removing the finalizers to allow the following delete remove the object.
// This step will fail if previous delete wrongly removed the object. The
// object will be deleted as part of the finalizer update.
for {
gottenNoxuInstance.SetFinalizers(nil)
_, err = noxuResourceClient.Update(gottenNoxuInstance)
if err == nil {
break
}
if !errors.IsConflict(err) {
require.NoError(t, err) // Fail on unexpected error
}
gottenNoxuInstance, err = noxuResourceClient.Get(name, metav1.GetOptions{})
require.NoError(t, err)
}
// Check that the object is actually gone.
_, err = noxuResourceClient.Get(name, metav1.GetOptions{})
require.Error(t, err)
require.True(t, errors.IsNotFound(err), "%#v", err)
}
func TestFinalizationAndDeletion(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
require.NoError(t, err)
defer tearDown()
// Create a CRD.
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
require.NoError(t, err)
// Create a CR with a finalizer.
ns := "not-the-default"
name := "foo123"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
instance := fixtures.NewNoxuInstance(ns, name)
instance.SetFinalizers([]string{"noxu.example.com/finalizer"})
createdNoxuInstance, err := instantiateCustomResource(t, instance, noxuResourceClient, noxuDefinition)
require.NoError(t, err)
// Delete a CR. Because there's a finalizer, it will not get deleted now.
uid := createdNoxuInstance.GetUID()
err = noxuResourceClient.Delete(name, &metav1.DeleteOptions{
Preconditions: &metav1.Preconditions{
UID: &uid,
},
})
require.NoError(t, err)
// Check is the CR scheduled for deletion.
gottenNoxuInstance, err := noxuResourceClient.Get(name, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, gottenNoxuInstance.GetDeletionTimestamp())
// Delete the CRD.
fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient)
// Check is CR still there after the CRD deletion.
gottenNoxuInstance, err = noxuResourceClient.Get(name, metav1.GetOptions{})
require.NoError(t, err)
// Update the CR to remove the finalizer.
for {
gottenNoxuInstance.SetFinalizers(nil)
_, err = noxuResourceClient.Update(gottenNoxuInstance)
if err == nil {
break
}
if !errors.IsConflict(err) {
require.NoError(t, err) // Fail on unexpected error
}
gottenNoxuInstance, err = noxuResourceClient.Get(name, metav1.GetOptions{})
require.NoError(t, err)
}
// Verify the CR is gone.
// It should return the NonFound error.
_, err = noxuResourceClient.Get(name, metav1.GetOptions{})
if !errors.IsNotFound(err) {
t.Fatalf("unable to delete cr: %v", err)
}
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
_, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(noxuDefinition.Name, metav1.GetOptions{})
return errors.IsNotFound(err), err
})
if !errors.IsNotFound(err) {
t.Fatalf("unable to delete crd: %v", err)
}
}

View File

@@ -0,0 +1,403 @@
/*
Copyright 2017 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 fixtures
import (
"fmt"
"time"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/scale"
)
const (
noxuInstanceNum int64 = 9223372036854775807
)
// NewRandomNameCustomResourceDefinition generates a CRD with random name to avoid name conflict in e2e tests
func NewRandomNameCustomResourceDefinition(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
// ensure the singular doesn't end in an s for now
gName := names.SimpleNameGenerator.GenerateName("foo") + "a"
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: gName + "s.mygroup.example.com"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Version: "v1beta1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: gName + "s",
Singular: gName,
Kind: gName,
ListKind: gName + "List",
},
Scope: scope,
},
}
}
// NewNoxuCustomResourceDefinition returns a WishIHadChosenNoxu CRD.
func NewNoxuCustomResourceDefinition(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,
},
}
}
// NewVersionedNoxuInstance returns a WishIHadChosenNoxu instance for a given version
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,
},
},
}
}
// NewNoxuInstance returns a WishIHadChosenNoxu instance for v1beta1.
func NewNoxuInstance(namespace, name string) *unstructured.Unstructured {
return NewVersionedNoxuInstance(namespace, name, "v1beta1")
}
// NewMultipleVersionNoxuCRD returns a WishIHadChosenNoxu with multiple versions.
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,
},
},
},
}
}
// NewNoxu2CustomResourceDefinition returns a WishIHadChosenNoxu2 CRD.
func NewNoxu2CustomResourceDefinition(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "noxus2.mygroup.example.com"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Version: "v1alpha1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "noxus2",
Singular: "nonenglishnoxu2",
Kind: "WishIHadChosenNoxu2",
ShortNames: []string{"foo", "bar", "abc", "def"},
ListKind: "Noxu2ItemList",
},
Scope: scope,
},
}
}
// NewCurletCustomResourceDefinition returns a Curlet CRD.
func NewCurletCustomResourceDefinition(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "curlets.mygroup.example.com"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Version: "v1beta1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "curlets",
Singular: "curlet",
Kind: "Curlet",
ListKind: "CurletList",
},
Scope: scope,
},
}
}
// NewCurletInstance returns a Curlet instance.
func NewCurletInstance(namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "Curlet",
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
"content": map[string]interface{}{
"key": "value",
},
},
}
}
func servedVersions(crd *apiextensionsv1beta1.CustomResourceDefinition) []string {
if len(crd.Spec.Versions) == 0 {
return []string{crd.Spec.Version}
}
var versions []string
for _, v := range crd.Spec.Versions {
if v.Served {
versions = append(versions, v.Name)
}
}
return versions
}
func existsInDiscovery(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface, version string) (bool, error) {
groupResource, err := apiExtensionsClient.Discovery().ServerResourcesForGroupVersion(crd.Spec.Group + "/" + version)
if err != nil {
if errors.IsNotFound(err) {
return false, nil
}
return false, err
}
for _, g := range groupResource.APIResources {
if g.Name == crd.Spec.Names.Plural {
return true, nil
}
}
return false, nil
}
// CreateNewCustomResourceDefinitionWatchUnsafe creates the CRD and makes sure
// the apiextension apiserver has installed the CRD. But it's not safe to watch
// the created CR. Please call CreateNewCustomResourceDefinition if you need to
// watch the CR.
func CreateNewCustomResourceDefinitionWatchUnsafe(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
crd, err := apiExtensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
if err != nil {
return nil, err
}
// wait until all resources appears in discovery
for _, version := range servedVersions(crd) {
err := wait.PollImmediate(500*time.Millisecond, 30*time.Second, func() (bool, error) {
return existsInDiscovery(crd, apiExtensionsClient, version)
})
if err != nil {
return nil, err
}
}
return crd, err
}
// CreateNewCustomResourceDefinition creates the given CRD and makes sure its watch cache is primed on the server.
func CreateNewCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface, dynamicClientSet dynamic.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
crd, err := CreateNewCustomResourceDefinitionWatchUnsafe(crd, apiExtensionsClient)
if err != nil {
return nil, err
}
// This is only for a test. We need the watch cache to have a resource version that works for the test.
// When new REST storage is created, the storage cacher for the CR starts asynchronously.
// REST API operations return like list use the RV of etcd, but the storage cacher's reflector's list
// can get a different RV because etcd can be touched in between the initial list operation (if that's what you're doing first)
// and the storage cache reflector starting.
// Later, you can issue a watch with the REST apis list.RV and end up earlier than the storage cacher.
// The general working model is that if you get a "resourceVersion too old" message, you re-list and rewatch.
// For this test, we'll actually cycle, "list/watch/create/delete" until we get an RV from list that observes the create and not an error.
// This way all the tests that are checking for watches don't have to worry about RV too old problems because crazy things *could* happen
// before like the created RV could be too old to watch.
err = wait.PollImmediate(500*time.Millisecond, 30*time.Second, func() (bool, error) {
return isWatchCachePrimed(crd, dynamicClientSet)
})
if err != nil {
return nil, err
}
return crd, nil
}
func resourceClientForVersion(crd *apiextensionsv1beta1.CustomResourceDefinition, dynamicClientSet dynamic.Interface, namespace, 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 dynamicClientSet.Resource(gvr).Namespace(namespace)
}
return dynamicClientSet.Resource(gvr)
}
// isWatchCachePrimed returns true if the watch is primed for an specified version of CRD watch
func isWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition, dynamicClientSet dynamic.Interface) (bool, error) {
ns := ""
if crd.Spec.Scope != apiextensionsv1beta1.ClusterScoped {
ns = "aval"
}
versions := servedVersions(crd)
if len(versions) == 0 {
return true, nil
}
resourceClient := resourceClientForVersion(crd, dynamicClientSet, ns, versions[0])
instanceName := "setup-instance"
instance := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": crd.Spec.Group + "/" + versions[0],
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"namespace": ns,
"name": instanceName,
},
"alpha": "foo_123",
"beta": 10,
"gamma": "bar",
"delta": "hello",
"epsilon": "foobar",
"spec": map[string]interface{}{},
},
}
createdInstance, err := resourceClient.Create(instance)
if err != nil {
return false, err
}
err = resourceClient.Delete(createdInstance.GetName(), nil)
if err != nil {
return false, err
}
// Wait for all versions of watch cache to be primed and also make sure we consumed the DELETE event for all
// versions so that any new watch with ResourceVersion=0 does not get those events. This is source of some flaky tests.
// When a client creates a watch with resourceVersion=0, it will get an ADD event for any existing objects
// but because they specified resourceVersion=0, there is no starting point in the cache buffer to return existing events
// from, thus the server will return anything from current head of the cache to the end. By accessing the delete
// events for all versions here, we make sure that the head of the cache is passed those events and they will not being
// delivered to any future watch with resourceVersion=0.
for _, v := range versions {
noxuWatch, err := resourceClientForVersion(crd, dynamicClientSet, ns, v).Watch(
metav1.ListOptions{ResourceVersion: createdInstance.GetResourceVersion()})
if err != nil {
return false, err
}
defer noxuWatch.Stop()
select {
case watchEvent := <-noxuWatch.ResultChan():
if watch.Error == watchEvent.Type {
return false, nil
}
if watch.Deleted != watchEvent.Type {
return false, fmt.Errorf("expected DELETE, but got %#v", watchEvent)
}
case <-time.After(5 * time.Second):
return false, fmt.Errorf("gave up waiting for watch event")
}
}
return true, nil
}
// DeleteCustomResourceDefinition deletes a CRD and waits until it disappears from discovery.
func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) error {
if err := apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Delete(crd.Name, nil); err != nil {
return err
}
for _, version := range servedVersions(crd) {
err := wait.PollImmediate(500*time.Millisecond, 30*time.Second, func() (bool, error) {
exists, err := existsInDiscovery(crd, apiExtensionsClient, version)
return !exists, err
})
if err != nil {
return err
}
}
return nil
}
// CreateNewScaleClient returns a scale client.
func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config) (scale.ScalesGetter, error) {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version)
if err != nil {
return nil, err
}
resources := []*restmapper.APIGroupResources{
{
Group: metav1.APIGroup{
Name: crd.Spec.Group,
Versions: []metav1.GroupVersionForDiscovery{
{Version: crd.Spec.Version},
},
PreferredVersion: metav1.GroupVersionForDiscovery{Version: crd.Spec.Version},
},
VersionedResources: map[string][]metav1.APIResource{
crd.Spec.Version: groupResource.APIResources,
},
},
}
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
resolver := scale.NewDiscoveryScaleKindResolver(discoveryClient)
return scale.NewForConfig(config, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver)
}

View File

@@ -0,0 +1,108 @@
/*
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 fixtures
import (
"io/ioutil"
"os"
"strings"
"github.com/pborman/uuid"
"k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
servertesting "k8s.io/apiextensions-apiserver/pkg/cmd/server/testing"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
)
// StartDefaultServer starts a test server.
func StartDefaultServer(t servertesting.Logger) (func(), *rest.Config, *options.CustomResourceDefinitionsServerOptions, error) {
// create kubeconfig which will not actually be used. But authz/authn needs it to startup.
fakeKubeConfig, err := ioutil.TempFile("", "kubeconfig")
fakeKubeConfig.WriteString(`
apiVersion: v1
kind: Config
clusters:
- cluster:
server: http://127.1.2.3:12345
name: integration
contexts:
- context:
cluster: integration
user: test
name: default-context
current-context: default-context
users:
- name: test
user:
password: test
username: test
`)
fakeKubeConfig.Close()
s, err := servertesting.StartTestServer(t, nil, []string{
"--etcd-prefix", uuid.New(),
"--etcd-servers", strings.Join(IntegrationEtcdServers(), ","),
"--authentication-skip-lookup",
"--authentication-kubeconfig", fakeKubeConfig.Name(),
"--authorization-kubeconfig", fakeKubeConfig.Name(),
"--kubeconfig", fakeKubeConfig.Name(),
"--disable-admission-plugins", "NamespaceLifecycle,MutatingAdmissionWebhook,ValidatingAdmissionWebhook",
}, nil)
if err != nil {
os.Remove(fakeKubeConfig.Name())
return nil, nil, nil, err
}
tearDownFn := func() {
defer os.Remove(fakeKubeConfig.Name())
s.TearDownFn()
}
return tearDownFn, s.ClientConfig, s.ServerOpts, nil
}
// StartDefaultServerWithClients starts a test server and returns clients for it.
func StartDefaultServerWithClients(t servertesting.Logger) (func(), clientset.Interface, dynamic.Interface, error) {
tearDown, config, _, err := StartDefaultServer(t)
if err != nil {
return nil, nil, nil, err
}
apiExtensionsClient, err := clientset.NewForConfig(config)
if err != nil {
tearDown()
return nil, nil, nil, err
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
tearDown()
return nil, nil, nil, err
}
return tearDown, apiExtensionsClient, dynamicClient, nil
}
// IntegrationEtcdServers returns etcd server URLs.
func IntegrationEtcdServers() []string {
if etcdURL, ok := os.LookupEnv("KUBE_INTEGRATION_ETCD_URL"); ok {
return []string{etcdURL}
}
return []string{"http://127.0.0.1:2379"}
}

View File

@@ -0,0 +1,94 @@
/*
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 (
"fmt"
"testing"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)
func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client dynamic.ResourceInterface, definition *apiextensionsv1beta1.CustomResourceDefinition) (*unstructured.Unstructured, error) {
return instantiateVersionedCustomResource(t, instanceToCreate, client, definition, definition.Spec.Versions[0].Name)
}
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 {
return newNamespacedCustomResourceVersionedClient(ns, client, crd, crd.Spec.Versions[0].Name)
}
// updateCustomResourceDefinitionWithRetry updates a CRD, retrying up to 5 times on version conflict errors.
func updateCustomResourceDefinitionWithRetry(client clientset.Interface, name string, update func(*apiextensionsv1beta1.CustomResourceDefinition)) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
for i := 0; i < 5; i++ {
crd, err := client.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get CustomResourceDefinition %q: %v", name, err)
}
update(crd)
crd, err = client.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
if err == nil {
return crd, nil
}
if !errors.IsConflict(err) {
return nil, fmt.Errorf("failed to update CustomResourceDefinition %q: %v", name, err)
}
}
return nil, fmt.Errorf("too many retries after conflicts updating CustomResourceDefinition %q", name)
}

View File

@@ -0,0 +1,164 @@
/*
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 (
"path"
"reflect"
"strings"
"testing"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/pkg/transport"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/dynamic"
)
func TestPostInvalidObjectMeta(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
obj := fixtures.NewNoxuInstance("default", "foo")
unstructured.SetNestedField(obj.UnstructuredContent(), int64(42), "metadata", "unknown")
unstructured.SetNestedField(obj.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "labels")
_, err = instantiateCustomResource(t, obj, noxuResourceClient, noxuDefinition)
if err == nil {
t.Fatalf("unexpected non-error, expected invalid labels to be rejected: %v", err)
}
if status, ok := err.(errors.APIStatus); !ok {
t.Fatalf("expected APIStatus error, but got: %#v", err)
} else if !errors.IsBadRequest(err) {
t.Fatalf("expected BadRequst error, but got: %v", errors.ReasonForError(err))
} else if !strings.Contains(status.Status().Message, "cannot be handled") {
t.Fatalf("expected 'cannot be handled' error message, got: %v", status.Status().Message)
}
unstructured.SetNestedField(obj.UnstructuredContent(), map[string]interface{}{"bar": "abc"}, "metadata", "labels")
obj, err = instantiateCustomResource(t, obj, noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if unknown, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
t.Errorf("unexpected error getting metadata.unknown: %v", err)
} else if found {
t.Errorf("unexpected metadata.unknown=%#v: expected this to be pruned", unknown)
}
}
func TestInvalidObjectMetaInStorage(t *testing.T) {
tearDown, config, options, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: noxuDefinition.Spec.Group, Resource: noxuDefinition.Spec.Names.Plural})
if err != nil {
t.Fatal(err)
}
tlsInfo := transport.TLSInfo{
CertFile: restOptions.StorageConfig.CertFile,
KeyFile: restOptions.StorageConfig.KeyFile,
CAFile: restOptions.StorageConfig.CAFile,
}
tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
t.Fatal(err)
}
etcdConfig := clientv3.Config{
Endpoints: restOptions.StorageConfig.ServerList,
TLS: tlsConfig,
}
etcdclient, err := clientv3.New(etcdConfig)
if err != nil {
t.Fatal(err)
}
t.Logf("Creating object with invalid labels manually in etcd")
original := fixtures.NewNoxuInstance("default", "foo")
unstructured.SetNestedField(original.UnstructuredContent(), int64(42), "metadata", "unknown")
unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "labels")
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := path.Join("/", restOptions.StorageConfig.Prefix, noxuDefinition.Spec.Group, "noxus/default/foo")
val, _ := json.Marshal(original.UnstructuredContent())
if _, err := etcdclient.Put(ctx, key, string(val)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Logf("Checking that ObjectMeta is pruned from unknown fields")
noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
obj, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if unknown, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
t.Errorf("unexpected error: %v", err)
} else if found {
t.Errorf("unexpected to find metadata.unknown=%#v", unknown)
}
t.Logf("Checking that ObjectMeta is pruned from invalid typed fields")
if labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "metadata", "labels"); err != nil {
t.Errorf("unexpected error: %v", err)
} else if found && !reflect.DeepEqual(labels, map[string]string{"bar": "abc"}) {
t.Errorf("unexpected to find metadata.lables=%#v", labels)
}
}

View File

@@ -0,0 +1,456 @@
/*
Copyright 2017 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 (
"context"
"encoding/json"
"fmt"
"os"
"path"
"reflect"
"testing"
"time"
"github.com/coreos/etcd/clientv3"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
)
func TestMultipleResourceInstances(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
ns := "not-the-default"
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
noxuList, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
noxuListListMeta, err := meta.ListAccessor(noxuList)
if err != nil {
t.Fatal(err)
}
noxuNamespacedWatch, err := noxuNamespacedResourceClient.Watch(metav1.ListOptions{ResourceVersion: noxuListListMeta.GetResourceVersion()})
if err != nil {
t.Fatal(err)
}
defer noxuNamespacedWatch.Stop()
instances := map[string]*struct {
Added bool
Deleted bool
Instance *unstructured.Unstructured
}{
"foo": {},
"bar": {},
}
for key, val := range instances {
val.Instance, err = instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, key), noxuNamespacedResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create Noxu Instance %q:%v", key, err)
}
}
addEvents := 0
for addEvents < len(instances) {
select {
case watchEvent := <-noxuNamespacedWatch.ResultChan():
if e, a := watch.Added, watchEvent.Type; e != a {
t.Fatalf("expected %v, got %v", e, a)
}
name, err := meta.NewAccessor().Name(watchEvent.Object)
if err != nil {
t.Fatalf("unable to retrieve object name:%v", err)
}
if instances[name].Added {
t.Fatalf("Add event already registered for %q", name)
}
instances[name].Added = true
addEvents++
case <-time.After(5 * time.Second):
t.Fatalf("missing watch event")
}
}
for key, val := range instances {
gottenNoxuInstace, err := noxuNamespacedResourceClient.Get(key, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := val.Instance, gottenNoxuInstace; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
listWithItem, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := len(instances), len(listWithItem.Items); e != a {
t.Errorf("expected %v, got %v", e, a)
}
for _, a := range listWithItem.Items {
if e := instances[a.GetName()].Instance; !reflect.DeepEqual(e, &a) {
t.Errorf("expected %v, got %v", e, a)
}
}
for key := range instances {
if err := noxuNamespacedResourceClient.Delete(key, nil); err != nil {
t.Fatalf("unable to delete %s:%v", key, err)
}
}
listWithoutItem, err := noxuNamespacedResourceClient.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)
}
deleteEvents := 0
for deleteEvents < len(instances) {
select {
case watchEvent := <-noxuNamespacedWatch.ResultChan():
if e, a := watch.Deleted, watchEvent.Type; e != a {
t.Errorf("expected %v, got %v", e, a)
break
}
name, err := meta.NewAccessor().Name(watchEvent.Object)
if err != nil {
t.Errorf("unable to retrieve object name:%v", err)
}
if instances[name].Deleted {
t.Errorf("Delete event already registered for %q", name)
}
instances[name].Deleted = true
deleteEvents++
case <-time.After(5 * time.Second):
t.Errorf("missing watch event")
}
}
}
func TestMultipleRegistration(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
ns := "not-the-default"
sameInstanceName := "foo"
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
createdNoxuInstance, err := instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, sameInstanceName), noxuNamespacedResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu Instance:%v", err)
}
gottenNoxuInstance, err := noxuNamespacedResourceClient.Get(sameInstanceName, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := createdNoxuInstance, gottenNoxuInstance; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
curletDefinition := fixtures.NewCurletCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
curletDefinition, err = fixtures.CreateNewCustomResourceDefinition(curletDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
curletNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, curletDefinition)
createdCurletInstance, err := instantiateCustomResource(t, fixtures.NewCurletInstance(ns, sameInstanceName), curletNamespacedResourceClient, curletDefinition)
if err != nil {
t.Fatalf("unable to create noxu Instance:%v", err)
}
gottenCurletInstance, err := curletNamespacedResourceClient.Get(sameInstanceName, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := createdCurletInstance, gottenCurletInstance; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
// now re-GET noxu
gottenNoxuInstance2, err := noxuNamespacedResourceClient.Get(sameInstanceName, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := createdNoxuInstance, gottenNoxuInstance2; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
func TestDeRegistrationAndReRegistration(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
ns := "not-the-default"
sameInstanceName := "foo"
func() {
noxuDefinition, err := fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
if _, err := instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, sameInstanceName), noxuNamespacedResourceClient, noxuDefinition); err != nil {
t.Fatal(err)
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
if _, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(noxuDefinition.Name, metav1.GetOptions{}); err == nil || !errors.IsNotFound(err) {
t.Fatalf("expected a NotFound error, got:%v", err)
}
if _, err = noxuNamespacedResourceClient.List(metav1.ListOptions{}); err == nil || !errors.IsNotFound(err) {
t.Fatalf("expected a NotFound error, got:%v", err)
}
if _, err = noxuNamespacedResourceClient.Get("foo", metav1.GetOptions{}); err == nil || !errors.IsNotFound(err) {
t.Fatalf("expected a NotFound error, got:%v", err)
}
}()
func() {
if _, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(noxuDefinition.Name, metav1.GetOptions{}); err == nil || !errors.IsNotFound(err) {
t.Fatalf("expected a NotFound error, got:%v", err)
}
noxuDefinition, err := fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
initialList, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if _, err = noxuNamespacedResourceClient.Get(sameInstanceName, metav1.GetOptions{}); err == nil || !errors.IsNotFound(err) {
t.Fatalf("expected a NotFound error, got:%v", err)
}
if e, a := 0, len(initialList.Items); e != a {
t.Fatalf("expected %v, got %v", e, a)
}
createdNoxuInstance, err := instantiateCustomResource(t, fixtures.NewNoxuInstance(ns, sameInstanceName), noxuNamespacedResourceClient, noxuDefinition)
if err != nil {
t.Fatal(err)
}
gottenNoxuInstance, err := noxuNamespacedResourceClient.Get(sameInstanceName, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := createdNoxuInstance, gottenNoxuInstance; !reflect.DeepEqual(e, a) {
t.Fatalf("expected %v, got %v", e, a)
}
listWithItem, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := 1, len(listWithItem.Items); e != a {
t.Fatalf("expected %v, got %v", e, a)
}
if e, a := *createdNoxuInstance, listWithItem.Items[0]; !reflect.DeepEqual(e, a) {
t.Fatalf("expected %v, got %v", e, a)
}
if err := noxuNamespacedResourceClient.Delete(sameInstanceName, nil); err != nil {
t.Fatal(err)
}
if _, err = noxuNamespacedResourceClient.Get(sameInstanceName, metav1.GetOptions{}); err == nil || !errors.IsNotFound(err) {
t.Fatalf("expected a NotFound error, got:%v", err)
}
listWithoutItem, err := noxuNamespacedResourceClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if e, a := 0, len(listWithoutItem.Items); e != a {
t.Fatalf("expected %v, got %v", e, a)
}
}()
}
func TestEtcdStorage(t *testing.T) {
tearDown, clientConfig, s, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := apiextensionsclientset.NewForConfig(clientConfig)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(clientConfig)
if err != nil {
t.Fatal(err)
}
etcdPrefix := s.RecommendedOptions.Etcd.StorageConfig.Prefix
ns1 := "another-default-is-possible"
curletDefinition := fixtures.NewCurletCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
curletDefinition, err = fixtures.CreateNewCustomResourceDefinition(curletDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
curletNamespacedResourceClient := newNamespacedCustomResourceClient(ns1, dynamicClient, curletDefinition)
if _, err := instantiateCustomResource(t, fixtures.NewCurletInstance(ns1, "bar"), curletNamespacedResourceClient, curletDefinition); err != nil {
t.Fatalf("unable to create curlet cluster scoped Instance:%v", err)
}
ns2 := "the-cruel-default"
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
noxuNamespacedResourceClient := newNamespacedCustomResourceClient(ns2, dynamicClient, noxuDefinition)
if _, err := instantiateCustomResource(t, fixtures.NewNoxuInstance(ns2, "foo"), noxuNamespacedResourceClient, noxuDefinition); err != nil {
t.Fatalf("unable to create noxu namespace scoped Instance:%v", err)
}
testcases := map[string]struct {
etcdPath string
expectedObject *metaObject
}{
"namespacedNoxuDefinition": {
etcdPath: "apiextensions.k8s.io/customresourcedefinitions/noxus.mygroup.example.com",
expectedObject: &metaObject{
Kind: "CustomResourceDefinition",
APIVersion: "apiextensions.k8s.io/v1beta1",
Metadata: Metadata{
Name: "noxus.mygroup.example.com",
Namespace: "",
SelfLink: "",
},
},
},
"namespacedNoxuInstance": {
etcdPath: "mygroup.example.com/noxus/the-cruel-default/foo",
expectedObject: &metaObject{
Kind: "WishIHadChosenNoxu",
APIVersion: "mygroup.example.com/v1beta1",
Metadata: Metadata{
Name: "foo",
Namespace: "the-cruel-default",
SelfLink: "", // TODO double check: empty?
},
},
},
"clusteredCurletDefinition": {
etcdPath: "apiextensions.k8s.io/customresourcedefinitions/curlets.mygroup.example.com",
expectedObject: &metaObject{
Kind: "CustomResourceDefinition",
APIVersion: "apiextensions.k8s.io/v1beta1",
Metadata: Metadata{
Name: "curlets.mygroup.example.com",
Namespace: "",
SelfLink: "",
},
},
},
"clusteredCurletInstance": {
etcdPath: "mygroup.example.com/curlets/bar",
expectedObject: &metaObject{
Kind: "Curlet",
APIVersion: "mygroup.example.com/v1beta1",
Metadata: Metadata{
Name: "bar",
Namespace: "",
SelfLink: "", // TODO double check: empty?
},
},
},
}
etcdURL, ok := os.LookupEnv("KUBE_INTEGRATION_ETCD_URL")
if !ok {
etcdURL = "http://127.0.0.1:2379"
}
cfg := clientv3.Config{
Endpoints: []string{etcdURL},
}
c, err := clientv3.New(cfg)
if err != nil {
t.Fatal(err)
}
kv := clientv3.NewKV(c)
for testName, tc := range testcases {
output, err := getFromEtcd(kv, etcdPrefix, tc.etcdPath)
if err != nil {
t.Fatalf("%s - no path gotten from etcd:%v", testName, err)
}
if e, a := tc.expectedObject, output; !reflect.DeepEqual(e, a) {
t.Errorf("%s - expected %#v\n got %#v\n", testName, e, a)
}
}
}
func getFromEtcd(keys clientv3.KV, prefix, localPath string) (*metaObject, error) {
internalPath := path.Join("/", prefix, localPath) // TODO: Double check, should we concatenate two prefixes?
response, err := keys.Get(context.Background(), internalPath)
if err != nil {
return nil, err
}
if response.More || response.Count != 1 || len(response.Kvs) != 1 {
return nil, fmt.Errorf("Invalid etcd response (not found == %v): %#v", response.Count == 0, response)
}
obj := &metaObject{}
if err := json.Unmarshal(response.Kvs[0].Value, obj); err != nil {
return nil, err
}
return obj, nil
}
type metaObject struct {
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
Metadata `json:"metadata,omitempty" protobuf:"bytes,3,opt,name=metadata"`
}
type Metadata struct {
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"`
SelfLink string `json:"selfLink,omitempty" protobuf:"bytes,3,opt,name=selfLink"`
}

View File

@@ -0,0 +1,744 @@
/*
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 (
"math"
"reflect"
"sort"
"strings"
"testing"
autoscaling "k8s.io/api/autoscaling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
)
var labelSelectorPath = ".status.labelSelector"
func NewNoxuSubresourcesCRD(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",
},
Scope: scope,
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &labelSelectorPath,
},
},
},
}
}
func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
"spec": map[string]interface{}{
"num": int64(10),
"replicas": int64(3),
},
"status": map[string]interface{}{
"replicas": int64(7),
},
},
}
}
func TestStatusSubresource(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// status should not be set after creation
if val, ok := gottenNoxuInstance.Object["status"]; ok {
t.Fatalf("status should not be set after creation, got %v", val)
}
// .status.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// UpdateStatus should not update spec.
// Check that .spec.num = 10 and .status.num = 20
updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != int64(10) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum)
}
statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
}
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// .status.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Update should not update status.
// Check that .spec.num = 40 and .status.num = 20
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update instance: %v", err)
}
specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != int64(40) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum)
}
statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
}
}
func TestScaleSubresource(t *testing.T) {
groupResource := schema.GroupResource{
Group: "mygroup.example.com",
Resource: "noxus",
}
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
// set invalid json path for specReplicasPath
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = "foo,bar"
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil {
t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec")
}
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = ".spec.replicas"
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
scaleClient, err := fixtures.CreateNewScaleClient(noxuDefinition, config)
if err != nil {
t.Fatal(err)
}
// set .status.labelSelector = bar
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", "status", "labelSelector")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = noxuResourceClient.UpdateStatus(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
// get the scale object
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
if err != nil {
t.Fatal(err)
}
if gottenScale.Spec.Replicas != 3 {
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas)
}
if gottenScale.Status.Selector != "bar" {
t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector)
}
// check self link
expectedSelfLink := "/apis/mygroup.example.com/v1beta1/namespaces/not-the-default/noxus/foo/scale"
if gottenScale.GetSelfLink() != expectedSelfLink {
t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink())
}
// update the scale object
// check that spec is updated, but status is not
gottenScale.Spec.Replicas = 5
gottenScale.Status.Selector = "baz"
updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale)
if err != nil {
t.Fatal(err)
}
if updatedScale.Spec.Replicas != 5 {
t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas)
}
if updatedScale.Status.Selector != "bar" {
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector)
}
// check that .spec.replicas = 5, but status is not updated
updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas")
if !found || err != nil {
t.Fatalf("unable to get .spec.replicas")
}
if specReplicas != 5 {
t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas)
}
statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, "status", "labelSelector")
if !found || err != nil {
t.Fatalf("unable to get .status.labelSelector")
}
if statusLabelSelector != "bar" {
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector)
}
// validate maximum value
// set .spec.replicas = math.MaxInt64
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = noxuResourceClient.Update(gottenNoxuInstance)
if err == nil {
t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647")
}
}
func TestValidationSchemaWithStatus(t *testing.T) {
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
// fields other than properties in root schema are not allowed
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Subresources = &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
}
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil {
t.Fatalf(`unexpected non-error, expected: must not have "additionalProperties" at the root of the schema if the status subresource is enabled`)
}
// make sure we are not restricting fields to properties even in subschemas
noxuDefinition.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Description: "Validation for spec",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"replicas": {
Type: "integer",
},
},
},
},
Required: []string{"spec"},
Description: "This is a description at the root of the schema",
}
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatalf("unable to created crd %v: %v", noxuDefinition.Name, err)
}
}
func TestValidateOnlyStatus(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
// UpdateStatus should validate only status
// 1. create a crd with max value of .spec.num = 10 and .status.num = 10
// 2. create a cr with .spec.num = 10 and .status.num = 10 (valid)
// 3. update the spec of the cr with .spec.num = 15 (spec is invalid), expect no error
// 4. update the spec of the cr with .spec.num = 15 (spec is invalid), expect error
// max value of spec.num = 10 and status.num = 10
schema := &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
"status": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
},
}
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: schema,
}
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
// set .spec.num = 10 and .status.num = 10
noxuInstance := NewNoxuSubresourceInstance(ns, "foo")
err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
createdNoxuInstance, err := instantiateCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
// update the spec with .spec.num = 15, expecting no error
err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num")
if err != nil {
t.Fatalf("unexpected error setting .spec.num: %v", err)
}
createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// update with .status.num = 15, expecting an error
err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num")
if err != nil {
t.Fatalf("unexpected error setting .status.num: %v", err)
}
createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance)
if err == nil {
t.Fatal("expected error, but got none")
}
statusError, isStatus := err.(*apierrors.StatusError)
if !isStatus || statusError == nil {
t.Fatalf("expected status error, got %T: %v", err, err)
}
if !strings.Contains(statusError.Error(), "Invalid value") {
t.Fatalf("expected 'Invalid value' in error, got: %v", err)
}
}
func TestSubresourcesDiscovery(t *testing.T) {
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
group := "mygroup.example.com"
version := "v1beta1"
resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
if err != nil {
t.Fatal(err)
}
if len(resources.APIResources) != 3 {
t.Fatalf("Expected exactly the resources \"noxus\", \"noxus/status\" and \"noxus/scale\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources)
}
// check discovery info for status
status := resources.APIResources[1]
if status.Name != "noxus/status" {
t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name)
}
if status.Namespaced != true {
t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced)
}
if status.Kind != "WishIHadChosenNoxu" {
t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind)
}
expectedVerbs := []string{"get", "patch", "update"}
sort.Strings(status.Verbs)
if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) {
t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs)
}
// check discovery info for scale
scale := resources.APIResources[2]
if scale.Group != autoscaling.GroupName {
t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group)
}
if scale.Version != "v1" {
t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version)
}
if scale.Name != "noxus/scale" {
t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name)
}
if scale.Namespaced != true {
t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced)
}
if scale.Kind != "Scale" {
t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind)
}
sort.Strings(scale.Verbs)
if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) {
t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs)
}
}
func TestGeneration(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
// .metadata.generation = 1
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if gottenNoxuInstance.GetGeneration() != 1 {
t.Fatalf(".metadata.generation should be 1 after creation")
}
// .status.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// UpdateStatus does not increment generation
updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
if updatedStatusInstance.GetGeneration() != 1 {
t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration())
}
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// .spec.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Update increments generation
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update instance: %v", err)
}
if updatedInstance.GetGeneration() != 2 {
t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration())
}
}
func TestSubresourcePatch(t *testing.T) {
groupResource := schema.GroupResource{
Group: "mygroup.example.com",
Resource: "noxus",
}
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
scaleClient, err := fixtures.CreateNewScaleClient(noxuDefinition, config)
if err != nil {
t.Fatal(err)
}
patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
patchedNoxuInstance, err := noxuResourceClient.Patch("foo", types.MergePatchType, patch, "status")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num should remain 10
specNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != 10 {
t.Fatalf(".spec.num: expected: %v, got: %v", 10, specNum)
}
// .status.num should be 999
statusNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != 999 {
t.Fatalf(".status.num: expected: %v, got: %v", 999, statusNum)
}
// this call waits for the resourceVersion to be reached in the cache before returning.
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
// and then the updated object shows a conflicting diff, which permanently fails the patch.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
// See https://issue.k8s.io/42644
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// no-op patch
_, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, "status")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// empty patch
_, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), "status")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`)
patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, "scale")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// this call waits for the resourceVersion to be reached in the cache before returning.
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
// and then the updated object shows a conflicting diff, which permanently fails the patch.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
// See https://issue.k8s.io/42644
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 7
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
if err != nil {
t.Fatal(err)
}
if gottenScale.Spec.Replicas != 7 {
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas)
}
if gottenScale.Status.Replicas != 0 {
t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas)
}
// no-op patch
_, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, "scale")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// empty patch
_, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), "scale")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// make sure strategic merge patch is not supported for both status and scale
_, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, "status")
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
}
_, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, "scale")
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
}
}

View File

@@ -0,0 +1,185 @@
/*
Copyright 2017 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 (
"fmt"
"testing"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
)
func newTableCRD() *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "tables.mygroup.example.com"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Version: "v1beta1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "tables",
Singular: "table",
Kind: "Table",
ListKind: "TablemList",
},
Scope: apiextensionsv1beta1.ClusterScoped,
AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
{Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"},
{Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"},
{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"},
},
},
}
}
func newTableInstance(name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "Table",
"metadata": map[string]interface{}{
"name": name,
},
"spec": map[string]interface{}{
"alpha": "foo_123",
"beta": 10,
"gamma": "bar",
"delta": "hello",
},
},
}
}
func TestTableGet(t *testing.T) {
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
crd := newTableCRD()
crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
t.Logf("table crd created: %#v", crd)
crClient := newNamespacedCustomResourceClient("", dynamicClient, crd)
foo, err := crClient.Create(newTableInstance("foo"))
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
t.Logf("foo created: %#v", foo.UnstructuredContent())
gv := schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}
gvk := gv.WithKind(crd.Spec.Names.Kind)
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
parameterCodec := runtime.NewParameterCodec(scheme)
metav1.AddToGroupVersion(scheme, gv)
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
crConfig := *config
crConfig.GroupVersion = &gv
crConfig.APIPath = "/apis"
crConfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs}
crRestClient, err := rest.RESTClientFor(&crConfig)
if err != nil {
t.Fatal(err)
}
ret, err := crRestClient.Get().
Resource(crd.Spec.Names.Plural).
SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
VersionedParams(&metav1beta1.TableOptions{}, parameterCodec).
Do().
Get()
if err != nil {
t.Fatalf("failed to list %v resources: %v", gvk, err)
}
tbl, ok := ret.(*metav1beta1.Table)
if !ok {
t.Fatalf("expected metav1beta1.Table, got %T", ret)
}
t.Logf("%v table list: %#v", gvk, tbl)
if got, expected := len(tbl.ColumnDefinitions), 5; got != expected {
t.Errorf("expected %d headers, got %d", expected, got)
} else {
alpha := metav1beta1.TableColumnDefinition{Name: "Alpha", Type: "string", Format: "", Description: "Custom resource definition column (in JSONPath format): .spec.alpha", Priority: 0}
if got, expected := tbl.ColumnDefinitions[2], alpha; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
beta := metav1beta1.TableColumnDefinition{Name: "Beta", Type: "integer", Format: "int64", Description: "the beta field", Priority: 42}
if got, expected := tbl.ColumnDefinitions[3], beta; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
gamma := metav1beta1.TableColumnDefinition{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values"}
if got, expected := tbl.ColumnDefinitions[4], gamma; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
}
if got, expected := len(tbl.Rows), 1; got != expected {
t.Errorf("expected %d rows, got %d", expected, got)
} else if got, expected := len(tbl.Rows[0].Cells), 5; got != expected {
t.Errorf("expected %d cells, got %d", expected, got)
} else {
if got, expected := tbl.Rows[0].Cells[0], "foo"; got != expected {
t.Errorf("expected cell[0] to equal %q, got %q", expected, got)
}
if got, expected := tbl.Rows[0].Cells[2], "foo_123"; got != expected {
t.Errorf("expected cell[2] to equal %q, got %q", expected, got)
}
if got, expected := tbl.Rows[0].Cells[3], int64(10); got != expected {
t.Errorf("expected cell[3] to equal %#v, got %#v", expected, got)
}
if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected {
t.Errorf("expected cell[3] to equal %#v although the type does not match the column, got %#v", expected, got)
}
}
}

View File

@@ -0,0 +1,431 @@
/*
Copyright 2017 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 (
"strings"
"testing"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/wait"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
)
func TestForProperValidationErrors(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
tests := []struct {
name string
instanceFn func() *unstructured.Unstructured
expectedError string
}{
{
name: "bad version",
instanceFn: func() *unstructured.Unstructured {
instance := fixtures.NewVersionedNoxuInstance(ns, "foo", "v2")
return instance
},
expectedError: "the API version in the data (mygroup.example.com/v2) does not match the expected API version (mygroup.example.com/v1beta1)",
},
{
name: "bad kind",
instanceFn: func() *unstructured.Unstructured {
instance := fixtures.NewNoxuInstance(ns, "foo")
instance.Object["kind"] = "SomethingElse"
return instance
},
expectedError: `SomethingElse.mygroup.example.com "foo" is invalid: kind: Invalid value: "SomethingElse": must be WishIHadChosenNoxu`,
},
}
for _, tc := range tests {
_, err := noxuResourceClient.Create(tc.instanceFn())
if err == nil {
t.Errorf("%v: expected %v", tc.name, tc.expectedError)
continue
}
// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
if !strings.Contains(err.Error(), tc.expectedError) {
t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
continue
}
}
}
func newNoxuValidationCRD(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",
},
Scope: apiextensionsv1beta1.NamespaceScoped,
Validation: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Required: []string{"alpha", "beta"},
AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{
Allows: true,
},
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"alpha": {
Description: "Alpha is an alphanumeric string with underscores",
Type: "string",
Pattern: "^[a-zA-Z0-9_]*$",
},
"beta": {
Description: "Minimum value of beta is 10",
Type: "number",
Minimum: float64Ptr(10),
},
"gamma": {
Description: "Gamma is restricted to foo, bar and baz",
Type: "string",
Enum: []apiextensionsv1beta1.JSON{
{
Raw: []byte(`"foo"`),
},
{
Raw: []byte(`"bar"`),
},
{
Raw: []byte(`"baz"`),
},
},
},
"delta": {
Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0",
AnyOf: []apiextensionsv1beta1.JSONSchemaProps{
{
Type: "string",
MaxLength: int64Ptr(5),
},
{
Type: "number",
Minimum: float64Ptr(0),
},
},
},
},
},
},
},
}
}
func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
"alpha": "foo_123",
"beta": 10,
"gamma": "bar",
"delta": "hello",
},
}
}
func TestCustomResourceValidation(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
}
func TestCustomResourceUpdateValidation(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// invalidate the instance
gottenNoxuInstance.Object = map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{
"namespace": "not-the-default",
"name": "foo",
},
"gamma": "bar",
"delta": "hello",
}
_, err = noxuResourceClient.Update(gottenNoxuInstance)
if err == nil {
t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance)
}
}
func TestCustomResourceValidationErrors(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
tests := []struct {
name string
instanceFn func() *unstructured.Unstructured
expectedError string
}{
{
name: "bad alpha",
instanceFn: func() *unstructured.Unstructured {
instance := newNoxuValidationInstance(ns, "foo")
instance.Object["alpha"] = "foo_123!"
return instance
},
expectedError: "alpha in body should match '^[a-zA-Z0-9_]*$'",
},
{
name: "bad beta",
instanceFn: func() *unstructured.Unstructured {
instance := newNoxuValidationInstance(ns, "foo")
instance.Object["beta"] = 5
return instance
},
expectedError: "beta in body should be greater than or equal to 10",
},
{
name: "bad gamma",
instanceFn: func() *unstructured.Unstructured {
instance := newNoxuValidationInstance(ns, "foo")
instance.Object["gamma"] = "qux"
return instance
},
expectedError: "gamma in body should be one of [foo bar baz]",
},
{
name: "bad delta",
instanceFn: func() *unstructured.Unstructured {
instance := newNoxuValidationInstance(ns, "foo")
instance.Object["delta"] = "foobarbaz"
return instance
},
expectedError: "must validate at least one schema (anyOf)\ndelta in body should be at most 5 chars long",
},
{
name: "absent alpha and beta",
instanceFn: func() *unstructured.Unstructured {
instance := newNoxuValidationInstance(ns, "foo")
instance.Object = map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{
"namespace": "not-the-default",
"name": "foo",
},
"gamma": "bar",
"delta": "hello",
}
return instance
},
expectedError: ".alpha in body is required\n.beta in body is required",
},
}
for _, tc := range tests {
_, err := noxuResourceClient.Create(tc.instanceFn())
if err == nil {
t.Errorf("%v: expected %v", tc.name, tc.expectedError)
continue
}
// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
if !strings.Contains(err.Error(), tc.expectedError) {
t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
continue
}
}
}
func TestCRValidationOnCRDUpdate(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
// set stricter schema
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"}
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
// CR is rejected
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err == nil {
t.Fatalf("unexpected non-error: CR should be rejected")
}
// update the CRD to a less stricter schema
_, err = updateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
crd.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta"}
})
if err != nil {
t.Fatal(err)
}
// CR is now accepted
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
_, err := noxuResourceClient.Create(newNoxuValidationInstance(ns, "foo"))
if statusError, isStatus := err.(*apierrors.StatusError); isStatus {
if strings.Contains(statusError.Error(), "is invalid") {
return false, nil
}
}
if err != nil {
return false, err
}
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
func TestForbiddenFieldsInSchema(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = false
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil {
t.Fatalf("unexpected non-error: additionalProperties cannot be set to false")
}
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
Type: "array",
UniqueItems: true,
}
noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = true
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil {
t.Fatalf("unexpected non-error: uniqueItems cannot be set to true")
}
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta")
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
Type: "array",
UniqueItems: false,
}
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil {
t.Fatal("unexpected non-error: $ref cannot be non-empty string")
}
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Ref = nil
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
}
func float64Ptr(f float64) *float64 {
return &f
}
func int64Ptr(f int64) *int64 {
return &f
}
func strPtr(str string) *string {
return &str
}

View File

@@ -0,0 +1,138 @@
/*
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"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestVersionedNamspacedScopedCRD(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
testSimpleCRUD(t, ns, noxuDefinition, dynamicClient)
}
func TestVersionedClusterScopedCRD(t *testing.T) {
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition := fixtures.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
ns := ""
testSimpleCRUD(t, ns, noxuDefinition, dynamicClient)
}
func TestStoragedVersionInNamespacedCRDStatus(t *testing.T) {
noxuDefinition := fixtures.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.NamespaceScoped)
ns := "not-the-default"
testStoragedVersionInCRDStatus(t, ns, noxuDefinition)
}
func TestStoragedVersionInClusterScopedCRDStatus(t *testing.T) {
noxuDefinition := fixtures.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,
},
}
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
noxuDefinition.Spec.Versions = versionsV1Beta1Storage
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
// The storage version list should be initilized to storage version
crd, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(noxuDefinition.Name, metav1.GetOptions{})
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 = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(noxuDefinition.Name, metav1.GetOptions{})
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 = fixtures.DeleteCustomResourceDefinition(crd, apiExtensionClient)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,541 @@
/*
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 (
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/ghodss/yaml"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
)
func TestYAML(t *testing.T) {
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
listKind := noxuDefinition.Spec.Names.ListKind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
rest := apiExtensionClient.Discovery().RESTClient()
// Discovery
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetAPIVersion() != "v1" || obj.GetKind() != "APIResourceList" {
t.Fatalf("unexpected discovery kind: %s", string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "groupVersion"); v != apiVersion || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Error
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "missingname").
DoRaw()
if !errors.IsNotFound(err) {
t.Fatalf("expected not found, got %v", err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" {
t.Fatalf("unexpected discovery kind: %s", string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "reason"); v != "NotFound" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
uid := types.UID("")
resourceVersion := ""
// Create
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
values:
numVal: 1
boolVal: true
stringVal: "1"`, apiVersion, kind))
result, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 1 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != true || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "1" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
uid = obj.GetUID()
resourceVersion = obj.GetResourceVersion()
}
// Get
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 1 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != true || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "1" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// List
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
listObj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if listObj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, listObj.GetAPIVersion())
}
if listObj.GetKind() != listKind {
t.Fatalf("expected %s, got %s", kind, listObj.GetKind())
}
items, ok, err := unstructured.NestedSlice(listObj.Object, "items")
if !ok || err != nil || len(items) != 1 {
t.Fatalf("expected one item, got %v %v %v", items, ok, err)
}
obj := unstructured.Unstructured{Object: items[0].(map[string]interface{})}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 1 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != true || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "1" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Watch rejects yaml (no streaming support)
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Param("watch", "true").
DoRaw()
if !errors.IsNotAcceptable(err) {
t.Fatalf("expected not acceptable error, got %v (%s)", err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" {
t.Fatalf("unexpected result: %s", string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "reason"); v != "NotAcceptable" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "code"); v != http.StatusNotAcceptable || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Update
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
uid: %s
resourceVersion: "%s"
values:
numVal: 2
boolVal: false
stringVal: "2"`, apiVersion, kind, uid, resourceVersion))
result, err := rest.Put().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "values", "numVal"); v != 2 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedBool(obj.Object, "values", "boolVal"); v != false || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "values", "stringVal"); v != "2" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if obj.GetUID() != uid {
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
}
}
// Patch rejects yaml requests (only JSON mime types are allowed)
{
yamlBody := []byte(fmt.Sprintf(`
values:
numVal: 3`))
result, err := rest.Patch(types.MergePatchType).
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest").
Body(yamlBody).
DoRaw()
if !errors.IsUnsupportedMediaType(err) {
t.Fatalf("Expected bad request, got %v\n%s", err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" {
t.Fatalf("expected %s %s, got %s %s", "v1", "Status", obj.GetAPIVersion(), obj.GetKind())
}
if v, ok, err := unstructured.NestedString(obj.Object, "reason"); v != "UnsupportedMediaType" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Delete
{
result, err := rest.Delete().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest").
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetAPIVersion() != "v1" || obj.GetKind() != "Status" {
t.Fatalf("unexpected response: %s", string(result))
}
if v, ok, err := unstructured.NestedString(obj.Object, "status"); v != "Success" || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
}
func TestYAMLSubresource(t *testing.T) {
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
rest := apiExtensionClient.Discovery().RESTClient()
uid := types.UID("")
resourceVersion := ""
// Create
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
spec:
replicas: 3`, apiVersion, kind))
result, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
uid = obj.GetUID()
resourceVersion = obj.GetResourceVersion()
}
// Get at /status
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Update at /status
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
uid: %s
resourceVersion: "%s"
spec:
replicas: 5
status:
replicas: 3`, apiVersion, kind, uid, resourceVersion))
result, err := rest.Put().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if obj.GetUID() != uid {
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
}
}
// Get at /scale
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "scale").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != "autoscaling/v1" {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != "Scale" {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
}
func decodeYAML(data []byte) (*unstructured.Unstructured, error) {
retval := &unstructured.Unstructured{Object: map[string]interface{}{}}
// ensure this isn't JSON
if json.Unmarshal(data, &retval.Object) == nil {
return nil, fmt.Errorf("data is JSON, not YAML: %s", string(data))
}
// ensure it is YAML
retval.Object = map[string]interface{}{}
if err := yaml.Unmarshal(data, &retval.Object); err != nil {
return nil, fmt.Errorf("error decoding YAML: %v\noriginal YAML: %s", err, string(data))
}
return retval, nil
}