diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index b8614b9838a..33d372a49e8 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -22,10 +22,11 @@ import ( fuzz "github.com/google/gofuzz" appsv1 "k8s.io/api/apps/v1" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" apitesting "k8s.io/apimachinery/pkg/api/apitesting" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" genericfuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" + metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" "k8s.io/apimachinery/pkg/runtime" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" admissionregistrationfuzzer "k8s.io/kubernetes/pkg/apis/admissionregistration/fuzzer" @@ -105,4 +106,5 @@ var FuzzerFuncs = fuzzer.MergeFuzzerFuncs( auditregistrationfuzzer.Funcs, storagefuzzer.Funcs, networkingfuzzer.Funcs, + metafuzzer.Funcs, ) diff --git a/pkg/printers/humanreadable.go b/pkg/printers/humanreadable.go index 5e51fb335c8..d94ba4c4cc9 100644 --- a/pkg/printers/humanreadable.go +++ b/pkg/printers/humanreadable.go @@ -509,14 +509,17 @@ func (h *HumanReadablePrinter) PrintTable(obj runtime.Object, options PrintOptio return nil, results[1].Interface().(error) } - columns := handler.columnDefinitions - if !options.Wide { - columns = make([]metav1beta1.TableColumnDefinition, 0, len(handler.columnDefinitions)) - for i := range handler.columnDefinitions { - if handler.columnDefinitions[i].Priority != 0 { - continue + var columns []metav1beta1.TableColumnDefinition + if !options.NoHeaders { + columns = handler.columnDefinitions + if !options.Wide { + columns = make([]metav1beta1.TableColumnDefinition, 0, len(handler.columnDefinitions)) + for i := range handler.columnDefinitions { + if handler.columnDefinitions[i].Priority != 0 { + continue + } + columns = append(columns, handler.columnDefinitions[i]) } - columns = append(columns, handler.columnDefinitions[i]) } } table := &metav1beta1.Table{ diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index b3980bcd526..782a569a3a0 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -430,14 +430,14 @@ func AddHandlers(h printers.PrintHandler) { h.TableHandler(controllerRevisionColumnDefinition, printControllerRevision) h.TableHandler(controllerRevisionColumnDefinition, printControllerRevisionList) - resorceQuotaColumnDefinitions := []metav1beta1.TableColumnDefinition{ + resourceQuotaColumnDefinitions := []metav1beta1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, {Name: "Request", Type: "string", Description: "Request represents a minimum amount of cpu/memory that a container may consume."}, {Name: "Limit", Type: "string", Description: "Limits control the maximum amount of cpu/memory that a container may use independent of contention on the node."}, } - h.TableHandler(resorceQuotaColumnDefinitions, printResourceQuota) - h.TableHandler(resorceQuotaColumnDefinitions, printResourceQuotaList) + h.TableHandler(resourceQuotaColumnDefinitions, printResourceQuota) + h.TableHandler(resourceQuotaColumnDefinitions, printResourceQuotaList) priorityClassColumnDefinitions := []metav1beta1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, @@ -456,6 +456,17 @@ func AddHandlers(h printers.PrintHandler) { h.TableHandler(runtimeClassColumnDefinitions, printRuntimeClass) h.TableHandler(runtimeClassColumnDefinitions, printRuntimeClassList) + volumeAttachmentColumnDefinitions := []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "Attacher", Type: "string", Format: "name", Description: storagev1.VolumeAttachmentSpec{}.SwaggerDoc()["attacher"]}, + {Name: "PV", Type: "string", Description: storagev1.VolumeAttachmentSource{}.SwaggerDoc()["persistentVolumeName"]}, + {Name: "Node", Type: "string", Description: storagev1.VolumeAttachmentSpec{}.SwaggerDoc()["nodeName"]}, + {Name: "Attached", Type: "boolean", Description: storagev1.VolumeAttachmentStatus{}.SwaggerDoc()["attached"]}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + h.TableHandler(volumeAttachmentColumnDefinitions, printVolumeAttachment) + h.TableHandler(volumeAttachmentColumnDefinitions, printVolumeAttachmentList) + AddDefaultHandlers(h) } @@ -1996,6 +2007,34 @@ func printRuntimeClassList(list *nodeapi.RuntimeClassList, options printers.Prin rows := make([]metav1beta1.TableRow, 0, len(list.Items)) for i := range list.Items { r, err := printRuntimeClass(&list.Items[i], options) + + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + +func printVolumeAttachment(obj *storage.VolumeAttachment, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { + row := metav1beta1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + + name := obj.Name + pvName := "" + if obj.Spec.Source.PersistentVolumeName != nil { + pvName = *obj.Spec.Source.PersistentVolumeName + } + row.Cells = append(row.Cells, name, obj.Spec.Attacher, pvName, obj.Spec.NodeName, obj.Status.Attached, translateTimestampSince(obj.CreationTimestamp)) + + return []metav1beta1.TableRow{row}, nil +} + +func printVolumeAttachmentList(list *storage.VolumeAttachmentList, options printers.PrintOptions) ([]metav1beta1.TableRow, error) { + rows := make([]metav1beta1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printVolumeAttachment(&list.Items[i], options) if err != nil { return nil, err } diff --git a/pkg/printers/storage/storage.go b/pkg/printers/storage/storage.go index f2b732974fe..c1bd30a94cf 100644 --- a/pkg/printers/storage/storage.go +++ b/pkg/printers/storage/storage.go @@ -18,6 +18,7 @@ package storage import ( "context" + "fmt" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" @@ -29,5 +30,16 @@ type TableConvertor struct { } func (c TableConvertor) ConvertToTable(ctx context.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1beta1.Table, error) { - return c.TablePrinter.PrintTable(obj, printers.PrintOptions{Wide: true}) + noHeaders := false + if tableOptions != nil { + switch t := tableOptions.(type) { + case *metav1beta1.TableOptions: + if t != nil { + noHeaders = t.NoHeaders + } + default: + return nil, fmt.Errorf("unrecognized type %T for table options, can't display tabular output", tableOptions) + } + } + return c.TablePrinter.PrintTable(obj, printers.PrintOptions{Wide: true, NoHeaders: noHeaders}) } diff --git a/pkg/registry/storage/volumeattachment/storage/BUILD b/pkg/registry/storage/volumeattachment/storage/BUILD index 9d1389020c7..fa25dcd6727 100644 --- a/pkg/registry/storage/volumeattachment/storage/BUILD +++ b/pkg/registry/storage/volumeattachment/storage/BUILD @@ -7,6 +7,9 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/apis/storage:go_default_library", + "//pkg/printers:go_default_library", + "//pkg/printers/internalversion:go_default_library", + "//pkg/printers/storage:go_default_library", "//pkg/registry/storage/volumeattachment:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/pkg/registry/storage/volumeattachment/storage/storage.go b/pkg/registry/storage/volumeattachment/storage/storage.go index 3727e98f54f..4f88077d1a0 100644 --- a/pkg/registry/storage/volumeattachment/storage/storage.go +++ b/pkg/registry/storage/volumeattachment/storage/storage.go @@ -25,6 +25,9 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" storageapi "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" "k8s.io/kubernetes/pkg/registry/storage/volumeattachment" ) @@ -50,6 +53,8 @@ func NewStorage(optsGetter generic.RESTOptionsGetter) *VolumeAttachmentStorage { UpdateStrategy: volumeattachment.Strategy, DeleteStrategy: volumeattachment.Strategy, ReturnDeletedObject: true, + + TableConvertor: printerstorage.TableConvertor{TablePrinter: printers.NewTablePrinter().With(printersinternal.AddHandlers)}, } options := &generic.StoreOptions{RESTOptions: optsGetter} if err := store.CompleteWithOptions(options); err != nil { diff --git a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json index 13f3a17793d..8710414a8b0 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json @@ -1110,6 +1110,10 @@ "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apimachinery/pkg/conversion", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/BUILD index dccf5a31445..dd46546d2f9 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/BUILD @@ -36,4 +36,11 @@ go_test( name = "go_default_test", srcs = ["tableconvertor_test.go"], embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//staging/src/k8s.io/client-go/util/jsonpath:go_default_library", + ], ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go index e1bed809d99..b5f1d52de56 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go @@ -76,9 +76,13 @@ type convertor struct { } func (c *convertor) ConvertToTable(ctx context.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1beta1.Table, error) { - table := &metav1beta1.Table{ - ColumnDefinitions: c.headers, + table := &metav1beta1.Table{} + opt, ok := tableOptions.(*metav1beta1.TableOptions) + noHeaders := ok && opt != nil && opt.NoHeaders + if !noHeaders { + table.ColumnDefinitions = c.headers } + if m, err := meta.ListAccessor(obj); err == nil { table.ResourceVersion = m.GetResourceVersion() table.SelfLink = m.GetSelfLink() diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor_test.go index 179aabb8ab7..092aae18e16 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor_test.go @@ -17,10 +17,17 @@ limitations under the License. package tableconvertor import ( + "context" "fmt" "reflect" "testing" "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/client-go/util/jsonpath" ) func Test_cellForJSONValue(t *testing.T) { @@ -66,3 +73,149 @@ func Test_cellForJSONValue(t *testing.T) { }) } } + +func Test_convertor_ConvertToTable(t *testing.T) { + type fields struct { + headers []metav1beta1.TableColumnDefinition + additionalColumns []*jsonpath.JSONPath + } + type args struct { + ctx context.Context + obj runtime.Object + tableOptions runtime.Object + } + tests := []struct { + name string + fields fields + args args + want *metav1beta1.Table + wantErr bool + }{ + { + name: "Return table for object", + fields: fields{ + headers: []metav1beta1.TableColumnDefinition{{Name: "name", Type: "string"}}, + }, + args: args{ + obj: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}, + }, + tableOptions: nil, + }, + want: &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{{Name: "name", Type: "string"}}, + Rows: []metav1beta1.TableRow{ + { + Cells: []interface{}{"blah"}, + Object: runtime.RawExtension{ + Object: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}, + }, + }, + }, + }, + }, + }, + { + name: "Return table for list", + fields: fields{ + headers: []metav1beta1.TableColumnDefinition{{Name: "name", Type: "string"}}, + }, + args: args{ + obj: &metav1beta1.PartialObjectMetadataList{ + Items: []*metav1beta1.PartialObjectMetadata{ + {ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}}, + {ObjectMeta: metav1.ObjectMeta{Name: "blah-2", CreationTimestamp: metav1.NewTime(time.Unix(2, 0))}}, + }, + }, + tableOptions: nil, + }, + want: &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{{Name: "name", Type: "string"}}, + Rows: []metav1beta1.TableRow{ + { + Cells: []interface{}{"blah"}, + Object: runtime.RawExtension{ + Object: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}, + }, + }, + }, + { + Cells: []interface{}{"blah-2"}, + Object: runtime.RawExtension{ + Object: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah-2", CreationTimestamp: metav1.NewTime(time.Unix(2, 0))}, + }, + }, + }, + }, + }, + }, + { + name: "Accept TableOptions", + fields: fields{ + headers: []metav1beta1.TableColumnDefinition{{Name: "name", Type: "string"}}, + }, + args: args{ + obj: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}, + }, + tableOptions: &metav1beta1.TableOptions{}, + }, + want: &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{{Name: "name", Type: "string"}}, + Rows: []metav1beta1.TableRow{ + { + Cells: []interface{}{"blah"}, + Object: runtime.RawExtension{ + Object: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}, + }, + }, + }, + }, + }, + }, + { + name: "Omit headers from TableOptions", + fields: fields{ + headers: []metav1beta1.TableColumnDefinition{{Name: "name", Type: "string"}}, + }, + args: args{ + obj: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}, + }, + tableOptions: &metav1beta1.TableOptions{NoHeaders: true}, + }, + want: &metav1beta1.Table{ + Rows: []metav1beta1.TableRow{ + { + Cells: []interface{}{"blah"}, + Object: runtime.RawExtension{ + Object: &metav1beta1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &convertor{ + headers: tt.fields.headers, + additionalColumns: tt.fields.additionalColumns, + } + got, err := c.ConvertToTable(tt.args.ctx, tt.args.obj, tt.args.tableOptions) + if (err != nil) != tt.wantErr { + t.Errorf("convertor.ConvertToTable() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("convertor.ConvertToTable() = %s", diff.ObjectReflectDiff(tt.want, got)) + } + }) + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go index b91b1e37fbf..d0c483e0a5f 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go @@ -23,7 +23,7 @@ import ( "strconv" "strings" - "github.com/google/gofuzz" + fuzz "github.com/google/gofuzz" apitesting "k8s.io/apimachinery/pkg/api/apitesting" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" @@ -282,8 +282,14 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { } } -func v1alpha1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { +func v1beta1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { return []interface{}{ + func(r *metav1beta1.TableOptions, c fuzz.Continue) { + c.FuzzNoCustom(r) + // NoHeaders is not serialized to the wire but is allowed within the versioned + // type because we don't use meta internal types in the client and API server. + r.NoHeaders = false + }, func(r *metav1beta1.TableRow, c fuzz.Continue) { c.Fuzz(&r.Object) c.Fuzz(&r.Conditions) @@ -326,5 +332,5 @@ func v1alpha1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { var Funcs = fuzzer.MergeFuzzerFuncs( genericFuzzerFuncs, v1FuzzerFuncs, - v1alpha1FuzzerFuncs, + v1beta1FuzzerFuncs, ) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/BUILD b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/BUILD index 9c8415aedaa..6cba6c2aa46 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/BUILD @@ -34,7 +34,10 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation:all-srcs", + ], tags = ["automanaged"], visibility = ["//visibility:public"], ) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/types.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/types.go index 344c533e13e..0462f087e64 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/types.go @@ -133,6 +133,10 @@ const ( // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type TableOptions struct { v1.TypeMeta `json:",inline"` + + // NoHeaders is only exposed for internal callers. + NoHeaders bool `json:"-"` + // includeObject decides whether to include each object along with its columnar information. // Specifying "None" will return no object, specifying "Object" will return the full object contents, and // specifying "Metadata" (the default) will return the object's metadata in the PartialObjectMetadata kind diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation/BUILD b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation/BUILD new file mode 100644 index 00000000000..f2515b972f0 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation/BUILD @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["validation.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation", + importpath = "k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation/validation.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation/validation.go new file mode 100644 index 00000000000..28256349018 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation/validation.go @@ -0,0 +1,33 @@ +/* +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 validation + +import ( + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateTableOptions returns any invalid flags on TableOptions. +func ValidateTableOptions(opts *metav1beta1.TableOptions) field.ErrorList { + var allErrs field.ErrorList + switch opts.IncludeObject { + case metav1beta1.IncludeMetadata, metav1beta1.IncludeNone, metav1beta1.IncludeObject, "": + default: + allErrs = append(allErrs, field.Invalid(field.NewPath("includeObject"), opts.IncludeObject, "must be 'Metadata', 'Object', 'None', or empty")) + } + return allErrs +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go index 1f7f662e075..eb284eac279 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go @@ -43,6 +43,8 @@ type TypeMeta struct { const ( ContentTypeJSON string = "application/json" ContentTypeYAML string = "application/yaml" + + ContentTypeProtobuf string = "application/vnd.kubernetes.protobuf" ) // RawExtension is used to hold extensions in external versions. diff --git a/staging/src/k8s.io/apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiserver/Godeps/Godeps.json index 49e1dfbeb2f..d289bc14e25 100644 --- a/staging/src/k8s.io/apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiserver/Godeps/Godeps.json @@ -1134,6 +1134,10 @@ "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apimachinery/pkg/conversion", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD index 1b0d7be70b1..ed2645ddaa6 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD @@ -46,6 +46,7 @@ go_test( "//staging/src/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/filters:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/testing:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index ed5d76dd7dd..bf6c0e69f1b 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -37,7 +37,7 @@ import ( "testing" "time" - "github.com/emicklei/go-restful" + restful "github.com/emicklei/go-restful" fuzzer "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" apiequality "k8s.io/apimachinery/pkg/api/equality" apierrs "k8s.io/apimachinery/pkg/api/errors" @@ -51,6 +51,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/streaming" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -64,6 +65,7 @@ import ( "k8s.io/apiserver/pkg/audit" auditpolicy "k8s.io/apiserver/pkg/audit/policy" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/request" genericapitesting "k8s.io/apiserver/pkg/endpoints/testing" @@ -1804,6 +1806,10 @@ func TestGetTable(t *testing.T) { accept: runtime.ContentTypeJSON + ";as=Table;v=v1;g=meta.k8s.io", statusCode: http.StatusNotAcceptable, }, + { + accept: runtime.ContentTypeProtobuf + ";as=Table;v=v1beta1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, { item: true, accept: runtime.ContentTypeJSON + ";as=Table;v=v1beta1;g=meta.k8s.io", @@ -1819,6 +1825,24 @@ func TestGetTable(t *testing.T) { }, }, }, + { + item: true, + accept: strings.Join([]string{ + runtime.ContentTypeProtobuf + ";as=Table;v=v1beta1;g=meta.k8s.io", + runtime.ContentTypeJSON + ";as=Table;v=v1beta1;g=meta.k8s.io", + }, ","), + expected: &metav1beta1.Table{ + TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1beta1"}, + ListMeta: metav1.ListMeta{ResourceVersion: "10", SelfLink: "/blah"}, + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]}, + {Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]}, + }, + Rows: []metav1beta1.TableRow{ + {Cells: []interface{}{"foo1", now.Time.UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}}, + }, + }, + }, { item: true, accept: runtime.ContentTypeJSON + ";as=Table;v=v1beta1;g=meta.k8s.io", @@ -1918,6 +1942,216 @@ func TestGetTable(t *testing.T) { } } +func TestWatchTable(t *testing.T) { + obj := genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", ResourceVersion: "10", SelfLink: "/blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0)), UID: types.UID("abcdef0123")}, + Other: "foo", + } + + m, err := meta.Accessor(&obj) + if err != nil { + t.Fatal(err) + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1beta1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + encodedBody, err := runtime.Encode(metainternalversion.Codecs.LegacyCodec(metav1beta1.SchemeGroupVersion), partial) + if err != nil { + t.Fatal(err) + } + // the codec includes a trailing newline that is not present during decode + encodedBody = bytes.TrimSpace(encodedBody) + + metaDoc := metav1.ObjectMeta{}.SwaggerDoc() + + s := metainternalversion.Codecs.SupportedMediaTypes()[0].Serializer + + tests := []struct { + accept string + params url.Values + send func(w *watch.FakeWatcher) + + expected []*metav1.WatchEvent + contentType string + statusCode int + item bool + }{ + { + accept: runtime.ContentTypeJSON + ";as=Table;v=v1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + accept: runtime.ContentTypeJSON + ";as=Table;v=v1beta1;g=meta.k8s.io", + send: func(w *watch.FakeWatcher) { + w.Add(&obj) + }, + expected: []*metav1.WatchEvent{ + { + Type: "ADDED", + Object: runtime.RawExtension{ + Raw: []byte(strings.TrimSpace(runtime.EncodeOrDie(s, &metav1beta1.Table{ + TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1beta1"}, + ListMeta: metav1.ListMeta{ResourceVersion: "10", SelfLink: "/blah"}, + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]}, + {Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]}, + }, + Rows: []metav1beta1.TableRow{ + {Cells: []interface{}{"foo1", time.Unix(1, 0).UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}}, + }, + }))), + }, + }, + }, + }, + { + accept: runtime.ContentTypeJSON + ";as=Table;v=v1beta1;g=meta.k8s.io", + send: func(w *watch.FakeWatcher) { + w.Add(&obj) + w.Modify(&obj) + }, + expected: []*metav1.WatchEvent{ + { + Type: "ADDED", + Object: runtime.RawExtension{ + Raw: []byte(strings.TrimSpace(runtime.EncodeOrDie(s, &metav1beta1.Table{ + TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1beta1"}, + ListMeta: metav1.ListMeta{ResourceVersion: "10", SelfLink: "/blah"}, + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]}, + {Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]}, + }, + Rows: []metav1beta1.TableRow{ + {Cells: []interface{}{"foo1", time.Unix(1, 0).UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}}, + }, + }))), + }, + }, + { + Type: "MODIFIED", + Object: runtime.RawExtension{ + Raw: []byte(strings.TrimSpace(runtime.EncodeOrDie(s, &metav1beta1.Table{ + TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1beta1"}, + ListMeta: metav1.ListMeta{ResourceVersion: "10", SelfLink: "/blah"}, + Rows: []metav1beta1.TableRow{ + {Cells: []interface{}{"foo1", time.Unix(1, 0).UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}}, + }, + }))), + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + storage := map[string]rest.Storage{} + simpleStorage := SimpleRESTStorage{ + item: obj, + list: []genericapitesting.Simple{obj}, + } + + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple", + namespace: "default", + } + if test.item { + selfLinker.expectedSet += "/id" + selfLinker.name = "id" + } + storage["simple"] = &simpleStorage + handler := handleLinker(storage, selfLinker) + server := httptest.NewServer(handler) + defer server.Close() + + var id string + if test.item { + id = "/id" + } + u, err := url.Parse(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple") + if err != nil { + t.Fatal(err) + } + if test.params == nil { + test.params = url.Values{} + } + if test.item { + test.params["fieldSelector"] = []string{fmt.Sprintf("metadata.name=%s", id)} + } + test.params["watch"] = []string{"1"} + + u.RawQuery = test.params.Encode() + req := &http.Request{Method: "GET", URL: u} + req.Header = http.Header{} + req.Header.Set("Accept", test.accept) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if test.statusCode != 0 { + if resp.StatusCode != test.statusCode { + t.Fatalf("%d: unexpected response: %#v", i, resp) + } + obj, _, err := extractBodyObject(resp, unstructured.UnstructuredJSONScheme) + if err != nil { + t.Fatalf("%d: unexpected body read error: %v", i, err) + } + gvk := schema.GroupVersionKind{Version: "v1", Kind: "Status"} + if obj.GetObjectKind().GroupVersionKind() != gvk { + t.Fatalf("%d: unexpected error body: %#v", i, obj) + } + return + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("%d: unexpected response: %#v", i, resp) + } + + go func() { + defer simpleStorage.fakeWatch.Stop() + test.send(simpleStorage.fakeWatch) + }() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + t.Logf("Body:\n%s", string(body)) + d := watcher(resp.Header.Get("Content-Type"), ioutil.NopCloser(bytes.NewReader(body))) + var actual []*metav1.WatchEvent + for { + var event metav1.WatchEvent + _, _, err := d.Decode(nil, &event) + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + actual = append(actual, &event) + } + if !reflect.DeepEqual(test.expected, actual) { + for i := range test.expected { + if i >= len(actual) { + break + } + t.Logf("%s", diff.StringDiff(string(test.expected[i].Object.Raw), string(actual[i].Object.Raw))) + } + t.Fatalf("unexpected: %s", diff.ObjectReflectDiff(test.expected, actual)) + } + }) + } +} + +func watcher(mediaType string, r io.ReadCloser) streaming.Decoder { + info, ok := runtime.SerializerInfoForMediaType(metainternalversion.Codecs.SupportedMediaTypes(), mediaType) + if !ok || info.StreamSerializer == nil { + panic(info) + } + streamSerializer := info.StreamSerializer + fr := streamSerializer.Framer.NewFrameReader(r) + d := streaming.NewDecoder(fr, streamSerializer.Serializer) + return d +} + func TestGetPartialObjectMetadata(t *testing.T) { now := metav1.Time{metav1.Now().Rfc3339Copy().Local()} storage := map[string]rest.Storage{} @@ -3618,7 +3852,7 @@ func (obj *UnregisteredAPIObject) DeepCopyObject() runtime.Object { func TestWriteJSONDecodeError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - responsewriters.WriteObjectNegotiated(codecs, newGroupVersion, w, req, http.StatusOK, &UnregisteredAPIObject{"Undecodable"}) + responsewriters.WriteObjectNegotiated(codecs, negotiation.DefaultEndpointRestrictions, newGroupVersion, w, req, http.StatusOK, &UnregisteredAPIObject{"Undecodable"}) })) defer server.Close() // Decode error response behavior is dictated by diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/group.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/group.go index 02330e9f3e7..7e9927a3a55 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/group.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/group.go @@ -69,5 +69,5 @@ func (s *APIGroupHandler) handle(req *restful.Request, resp *restful.Response) { } func (s *APIGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, w, req, http.StatusOK, &s.group) + responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, w, req, http.StatusOK, &s.group) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/legacy.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/legacy.go index 837cd0130e5..b33ecec6532 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/legacy.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/legacy.go @@ -72,5 +72,5 @@ func (s *legacyRootAPIHandler) handle(req *restful.Request, resp *restful.Respon Versions: []string{"v1"}, } - responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, apiVersions) + responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, apiVersions) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/root.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/root.go index 7ed64a9f579..beba9c8a41d 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/root.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/root.go @@ -111,7 +111,7 @@ func (s *rootAPIsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) groups[i].ServerAddressByClientCIDRs = serverCIDR } - responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, resp, req, http.StatusOK, &metav1.APIGroupList{Groups: groups}) + responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, resp, req, http.StatusOK, &metav1.APIGroupList{Groups: groups}) } func (s *rootAPIsHandler) restfulHandle(req *restful.Request, resp *restful.Response) { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/version.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/version.go index aadfc7a5b49..0976041bff0 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/version.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/version.go @@ -78,6 +78,6 @@ func (s *APIVersionHandler) handle(req *restful.Request, resp *restful.Response) } func (s *APIVersionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, w, req, http.StatusOK, + responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, w, req, http.StatusOK, &metav1.APIResourceList{GroupVersion: s.groupVersion.String(), APIResources: s.apiResourceLister.ListAPIResources()}) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index a0e445f715e..55998af532b 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -61,6 +61,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go index cfa8a6872b8..efb839657b7 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go @@ -257,7 +257,7 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch } requestInfo, _ := request.RequestInfoFrom(ctx) metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() { - serveWatch(watcher, scope, req, w, timeout) + serveWatch(watcher, scope, outputMediaType, req, w, timeout) }) return } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go index 9a38abddbcc..86faf525df1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go @@ -47,6 +47,34 @@ func (e errNotAcceptable) Status() metav1.Status { } } +// errNotAcceptableConversion indicates Accept negotiation has failed specifically +// for a conversion to a known type. +type errNotAcceptableConversion struct { + target string + accepted []string +} + +// NewNotAcceptableConversionError returns an error indicating that the desired +// API transformation to the target group version kind string is not accepted and +// only the listed mime types are allowed. This is temporary while Table does not +// yet support protobuf encoding. +func NewNotAcceptableConversionError(target string, accepted []string) error { + return errNotAcceptableConversion{target, accepted} +} + +func (e errNotAcceptableConversion) Error() string { + return fmt.Sprintf("only the following media types are accepted when converting to %s: %v", e.target, strings.Join(e.accepted, ", ")) +} + +func (e errNotAcceptableConversion) Status() metav1.Status { + return metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotAcceptable, + Reason: metav1.StatusReasonNotAcceptable, + Message: e.Error(), + } +} + // errUnsupportedMediaType indicates Content-Type is not recognized type errUnsupportedMediaType struct { accepted []string diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go index ec8861cc63f..b513eb5edc8 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go @@ -56,15 +56,9 @@ func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer return mediaType, info, nil } -// NegotiateOutputSerializer returns a serializer for the output. -func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - _, info, err := NegotiateOutputMediaType(req, ns, DefaultEndpointRestrictions) - return info, err -} - -// NegotiateOutputStreamSerializer returns a stream serializer for the given request. -func NegotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), DefaultEndpointRestrictions) +// NegotiateOutputMediaTypeStream returns a stream serializer for the given request. +func NegotiateOutputMediaTypeStream(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (runtime.SerializerInfo, error) { + mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), restrictions) if !ok || mediaType.Accepted.Serializer.StreamSerializer == nil { _, supported := MediaTypesForSerializer(ns) return runtime.SerializerInfo{}, NewNotAcceptableError(supported) @@ -124,7 +118,7 @@ func isPrettyPrint(req *http.Request) bool { type EndpointRestrictions interface { // AllowsConversion should return true if the specified group version kind // is an allowed target object. - AllowsConversion(schema.GroupVersionKind) bool + AllowsConversion(target schema.GroupVersionKind, mimeType, mimeSubType string) bool // AllowsServerVersion should return true if the specified version is valid // for the server group. AllowsServerVersion(version string) bool @@ -139,9 +133,11 @@ var DefaultEndpointRestrictions = emptyEndpointRestrictions{} type emptyEndpointRestrictions struct{} -func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool { return false } -func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false } -func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" } +func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind, string, string) bool { + return false +} +func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false } +func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" } // AcceptedMediaType contains information about a valid media type that the // server can serialize. @@ -240,7 +236,7 @@ func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType } } - if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert) { + if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert, accepts.Type, accepts.SubType) { return MediaTypeOptions{}, false } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate_test.go index e69ba02af03..6d9bcff7bd2 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate_test.go @@ -231,7 +231,7 @@ func TestNegotiate(t *testing.T) { req = &http.Request{Header: http.Header{}} req.Header.Set("Accept", test.accept) } - s, err := NegotiateOutputSerializer(req, test.ns) + _, s, err := NegotiateOutputMediaType(req, test.ns, DefaultEndpointRestrictions) switch { case err == nil && test.errFn != nil: t.Errorf("%d: failed: expected error", i) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go index f9f363840f7..c9e0d23ff63 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go @@ -26,86 +26,97 @@ import ( metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" ) +// transformObject takes the object as returned by storage and ensures it is in +// the client's desired form, as well as ensuring any API level fields like self-link +// are properly set. +func transformObject(ctx context.Context, obj runtime.Object, opts interface{}, mediaType negotiation.MediaTypeOptions, scope RequestScope, req *http.Request) (runtime.Object, error) { + if _, ok := obj.(*metav1.Status); ok { + return obj, nil + } + if err := setObjectSelfLink(ctx, obj, req, scope.Namer); err != nil { + return nil, err + } + + switch target := mediaType.Convert; { + case target == nil: + return obj, nil + + case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + return asV1Beta1PartialObjectMetadata(obj) + + case target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + return asV1Beta1PartialObjectMetadataList(obj) + + case target.Kind == "Table" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + options, ok := opts.(*metav1beta1.TableOptions) + if !ok { + return nil, fmt.Errorf("unexpected TableOptions, got %T", opts) + } + return asV1Beta1Table(ctx, obj, options, scope) + + default: + accepted, _ := negotiation.MediaTypesForSerializer(metainternalversion.Codecs) + err := negotiation.NewNotAcceptableError(accepted) + return nil, err + } +} + +// optionsForTransform will load and validate any additional query parameter options for +// a conversion or return an error. +func optionsForTransform(mediaType negotiation.MediaTypeOptions, req *http.Request) (interface{}, error) { + switch target := mediaType.Convert; { + case target == nil: + case target.Kind == "Table" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + opts := &metav1beta1.TableOptions{} + if err := metav1beta1.ParameterCodec.DecodeParameters(req.URL.Query(), metav1beta1.SchemeGroupVersion, opts); err != nil { + return nil, err + } + switch errs := validation.ValidateTableOptions(opts); len(errs) { + case 0: + return opts, nil + case 1: + return nil, errors.NewBadRequest(fmt.Sprintf("Unable to convert to Table as requested: %v", errs[0].Error())) + default: + return nil, errors.NewBadRequest(fmt.Sprintf("Unable to convert to Table as requested: %v", errs)) + } + } + return nil, nil +} + +// targetEncodingForTransform returns the appropriate serializer for the input media type +func targetEncodingForTransform(scope *RequestScope, mediaType negotiation.MediaTypeOptions, req *http.Request) (schema.GroupVersionKind, runtime.NegotiatedSerializer, bool) { + switch target := mediaType.Convert; { + case target == nil: + case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1beta1.SchemeGroupVersion, + target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1beta1.SchemeGroupVersion, + target.Kind == "Table" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: + return *target, metainternalversion.Codecs, true + } + return scope.Kind, scope.Serializer, false +} + // transformResponseObject takes an object loaded from storage and performs any necessary transformations. // Will write the complete response object. func transformResponseObject(ctx context.Context, scope RequestScope, req *http.Request, w http.ResponseWriter, statusCode int, mediaType negotiation.MediaTypeOptions, result runtime.Object) { - // status objects are ignored for transformation - if _, ok := result.(*metav1.Status); ok { - responsewriters.WriteObject(statusCode, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) - return - } - - // ensure the self link and empty list array are set - if err := setObjectSelfLink(ctx, result, req, scope.Namer); err != nil { + options, err := optionsForTransform(mediaType, req) + if err != nil { scope.err(err, w, req) return } - - trace := scope.Trace - - // If conversion was allowed by the scope, perform it before writing the response - switch target := mediaType.Convert; { - - case target == nil: - trace.Step("Writing response") - responsewriters.WriteObject(statusCode, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) - - case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: - partial, err := asV1Beta1PartialObjectMetadata(result) - if err != nil { - scope.err(err, w, req) - return - } - - if err := writeMetaInternalVersion(partial, statusCode, w, req, &scope, target.GroupVersion()); err != nil { - scope.err(err, w, req) - return - } - - case target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: - trace.Step("Processing list items") - partial, err := asV1Beta1PartialObjectMetadataList(result) - if err != nil { - scope.err(err, w, req) - return - } - - if err := writeMetaInternalVersion(partial, statusCode, w, req, &scope, target.GroupVersion()); err != nil { - scope.err(err, w, req) - return - } - - case target.Kind == "Table" && target.GroupVersion() == metav1beta1.SchemeGroupVersion: - opts := &metav1beta1.TableOptions{} - trace.Step("Decoding parameters") - if err := metav1beta1.ParameterCodec.DecodeParameters(req.URL.Query(), metav1beta1.SchemeGroupVersion, opts); err != nil { - scope.err(err, w, req) - return - } - - table, err := asV1Beta1Table(ctx, result, opts, scope) - if err != nil { - scope.err(err, w, req) - return - } - - if err := writeMetaInternalVersion(table, statusCode, w, req, &scope, target.GroupVersion()); err != nil { - scope.err(err, w, req) - return - } - - default: - // this block should only be hit if scope AllowsConversion is incorrect - accepted, _ := negotiation.MediaTypesForSerializer(metainternalversion.Codecs) - err := negotiation.NewNotAcceptableError(accepted) + obj, err := transformObject(ctx, result, options, mediaType, scope, req) + if err != nil { scope.err(err, w, req) + return } + kind, serializer, _ := targetEncodingForTransform(&scope, mediaType, req) + responsewriters.WriteObjectNegotiated(serializer, &scope, kind.GroupVersion(), w, req, statusCode, obj) } // errNotAcceptable indicates Accept negotiation has failed @@ -131,15 +142,11 @@ func (e errNotAcceptable) Status() metav1.Status { } func asV1Beta1Table(ctx context.Context, result runtime.Object, opts *metav1beta1.TableOptions, scope RequestScope) (runtime.Object, error) { - trace := scope.Trace - - trace.Step("Converting to table") table, err := scope.TableConvertor.ConvertToTable(ctx, result, opts) if err != nil { return nil, err } - trace.Step("Processing rows") for i := range table.Rows { item := &table.Rows[i] switch opts.IncludeObject { @@ -161,7 +168,6 @@ func asV1Beta1Table(ctx context.Context, result runtime.Object, opts *metav1beta case metav1beta1.IncludeNone: item.Object.Object = nil default: - // TODO: move this to validation on the table options? err = errors.NewBadRequest(fmt.Sprintf("unrecognized includeObject value: %q", opts.IncludeObject)) return nil, err } @@ -172,7 +178,6 @@ func asV1Beta1Table(ctx context.Context, result runtime.Object, opts *metav1beta func asV1Beta1PartialObjectMetadata(result runtime.Object) (runtime.Object, error) { if meta.IsListType(result) { - // TODO: this should be calculated earlier err := newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadata, but the requested object is a list (%T)", result)) return nil, err } @@ -187,7 +192,6 @@ func asV1Beta1PartialObjectMetadata(result runtime.Object) (runtime.Object, erro func asV1Beta1PartialObjectMetadataList(result runtime.Object) (runtime.Object, error) { if !meta.IsListType(result) { - // TODO: this should be calculated earlier return nil, newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadataList, but the requested object is not a list (%T)", result)) } list := &metav1beta1.PartialObjectMetadataList{} @@ -206,14 +210,3 @@ func asV1Beta1PartialObjectMetadataList(result runtime.Object) (runtime.Object, } return list, nil } - -func writeMetaInternalVersion(obj runtime.Object, statusCode int, w http.ResponseWriter, req *http.Request, restrictions negotiation.EndpointRestrictions, target schema.GroupVersion) error { - // renegotiate under the internal version - _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, restrictions) - if err != nil { - return err - } - encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, target) - responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, obj) - return nil -} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go index 5a811401cf3..472258bb0d8 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go @@ -55,23 +55,6 @@ func (w httpResponseWriterWithInit) Write(b []byte) (n int, err error) { return w.innerW.Write(b) } -// WriteObject renders a returned runtime.Object to the response as a stream or an encoded object. If the object -// returned by the response implements rest.ResourceStreamer that interface will be used to render the -// response. The Accept header and current API version will be passed in, and the output will be copied -// directly to the response body. If content type is returned it is used, otherwise the content type will -// be "application/octet-stream". All other objects are sent to standard JSON serialization. -func WriteObject(statusCode int, gv schema.GroupVersion, s runtime.NegotiatedSerializer, object runtime.Object, w http.ResponseWriter, req *http.Request) { - stream, ok := object.(rest.ResourceStreamer) - if ok { - requestInfo, _ := request.RequestInfoFrom(req.Context()) - metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() { - StreamObject(statusCode, gv, s, stream, w, req) - }) - return - } - WriteObjectNegotiated(s, gv, w, req, statusCode, object) -} - // StreamObject performs input stream negotiation from a ResourceStreamer and writes that to the response. // If the client requests a websocket upgrade, negotiate for a websocket reader protocol (because many // browser clients cannot easily handle binary streaming protocols). @@ -123,9 +106,17 @@ func SerializeObject(mediaType string, encoder runtime.Encoder, innerW http.Resp } // WriteObjectNegotiated renders an object in the content type negotiated by the client. -// The context is optional and can be nil. -func WriteObjectNegotiated(s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) { - serializer, err := negotiation.NegotiateOutputSerializer(req, s) +func WriteObjectNegotiated(s runtime.NegotiatedSerializer, restrictions negotiation.EndpointRestrictions, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) { + stream, ok := object.(rest.ResourceStreamer) + if ok { + requestInfo, _ := request.RequestInfoFrom(req.Context()) + metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() { + StreamObject(statusCode, gv, s, stream, w, req) + }) + return + } + + _, serializer, err := negotiation.NegotiateOutputMediaType(req, s, restrictions) if err != nil { // if original statusCode was not successful we need to return the original error // we cannot hide it behind negotiation problems @@ -162,7 +153,7 @@ func ErrorNegotiated(err error, s runtime.NegotiatedSerializer, gv schema.GroupV return code } - WriteObjectNegotiated(s, gv, w, req, code, status) + WriteObjectNegotiated(s, negotiation.DefaultEndpointRestrictions, gv, w, req, code, status) return code } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 74bb39c6d42..f482ae317b3 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -79,12 +79,14 @@ func (scope *RequestScope) err(err error, w http.ResponseWriter, req *http.Reque responsewriters.ErrorNegotiated(err, scope.Serializer, scope.Kind.GroupVersion(), w, req) } -func (scope *RequestScope) AllowsConversion(gvk schema.GroupVersionKind) bool { +func (scope *RequestScope) AllowsConversion(gvk schema.GroupVersionKind, mimeType, mimeSubType string) bool { // TODO: this is temporary, replace with an abstraction calculated at endpoint installation time if gvk.GroupVersion() == metav1beta1.SchemeGroupVersion { switch gvk.Kind { case "Table": - return scope.TableConvertor != nil + return scope.TableConvertor != nil && + mimeType == "application" && + (mimeSubType == "json" || mimeSubType == "yaml") case "PartialObjectMetadata", "PartialObjectMetadataList": // TODO: should delineate between lists and non-list endpoints return true @@ -172,7 +174,7 @@ type responder struct { } func (r *responder) Object(statusCode int, obj runtime.Object) { - responsewriters.WriteObject(statusCode, r.scope.Kind.GroupVersion(), r.scope.Serializer, obj, r.w, r.req) + responsewriters.WriteObjectNegotiated(r.scope.Serializer, &r.scope, r.scope.Kind.GroupVersion(), r.w, r.req, statusCode, obj) } func (r *responder) Error(err error) { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go index 0b4e7355621..301423d8480 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go @@ -25,13 +25,13 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/streaming" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/metrics" - "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/server/httplog" "k8s.io/apiserver/pkg/util/wsstream" @@ -61,42 +61,56 @@ func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) { return t.C, t.Stop } -// serveWatch handles serving requests to the server +// serveWatch will serve a watch response. // TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled. -func serveWatch(watcher watch.Interface, scope RequestScope, req *http.Request, w http.ResponseWriter, timeout time.Duration) { - // negotiate for the stream serializer - serializer, err := negotiation.NegotiateOutputStreamSerializer(req, scope.Serializer) +func serveWatch(watcher watch.Interface, scope RequestScope, mediaTypeOptions negotiation.MediaTypeOptions, req *http.Request, w http.ResponseWriter, timeout time.Duration) { + options, err := optionsForTransform(mediaTypeOptions, req) + if err != nil { + scope.err(err, w, req) + return + } + + // negotiate for the stream serializer from the scope's serializer + serializer, err := negotiation.NegotiateOutputMediaTypeStream(req, scope.Serializer, &scope) if err != nil { scope.err(err, w, req) return } framer := serializer.StreamSerializer.Framer streamSerializer := serializer.StreamSerializer.Serializer - embedded := serializer.Serializer + encoder := scope.Serializer.EncoderForVersion(streamSerializer, scope.Kind.GroupVersion()) + useTextFraming := serializer.EncodesAsText if framer == nil { scope.err(fmt.Errorf("no framer defined for %q available for embedded encoding", serializer.MediaType), w, req) return } - encoder := scope.Serializer.EncoderForVersion(streamSerializer, scope.Kind.GroupVersion()) - - useTextFraming := serializer.EncodesAsText - - // find the embedded serializer matching the media type - embeddedEncoder := scope.Serializer.EncoderForVersion(embedded, scope.Kind.GroupVersion()) - // TODO: next step, get back mediaTypeOptions from negotiate and return the exact value here mediaType := serializer.MediaType if mediaType != runtime.ContentTypeJSON { mediaType += ";stream=watch" } - ctx := req.Context() - requestInfo, ok := request.RequestInfoFrom(ctx) - if !ok { - scope.err(fmt.Errorf("missing requestInfo"), w, req) - return + // locate the appropriate embedded encoder based on the transform + var embeddedEncoder runtime.Encoder + contentKind, contentSerializer, transform := targetEncodingForTransform(&scope, mediaTypeOptions, req) + if transform { + var embedded runtime.Serializer + for _, supported := range contentSerializer.SupportedMediaTypes() { + if supported.MediaType == serializer.MediaType { + embedded = supported.Serializer + } + } + if embedded == nil { + scope.err(fmt.Errorf("no encoder for %q exists in the requested target %#v", serializer.MediaType, contentSerializer), w, req) + return + } + embeddedEncoder = contentSerializer.EncoderForVersion(embedded, contentKind.GroupVersion()) + } else { + embeddedEncoder = scope.Serializer.EncoderForVersion(serializer.Serializer, contentKind.GroupVersion()) } + ctx := req.Context() + server := &WatchServer{ Watching: watcher, Scope: scope, @@ -106,10 +120,20 @@ func serveWatch(watcher watch.Interface, scope RequestScope, req *http.Request, Framer: framer, Encoder: encoder, EmbeddedEncoder: embeddedEncoder, - Fixup: func(obj runtime.Object) { - if err := setSelfLink(obj, requestInfo, scope.Namer); err != nil { - utilruntime.HandleError(fmt.Errorf("failed to set link for object %v: %v", reflect.TypeOf(obj), err)) + + Fixup: func(obj runtime.Object) runtime.Object { + result, err := transformObject(ctx, obj, options, mediaTypeOptions, scope, req) + if err != nil { + utilruntime.HandleError(fmt.Errorf("failed to transform object %v: %v", reflect.TypeOf(obj), err)) + return obj } + // When we are transformed to a table, use the table options as the state for whether we + // should print headers - on watch, we only want to print table headers on the first object + // and omit them on subsequent events. + if tableOptions, ok := options.(*metav1beta1.TableOptions); ok { + tableOptions.NoHeaders = true + } + return result }, TimeoutFactory: &realTimeoutFactory{timeout}, @@ -133,7 +157,8 @@ type WatchServer struct { Encoder runtime.Encoder // used to encode the nested object in the watch stream EmbeddedEncoder runtime.Encoder - Fixup func(runtime.Object) + // used to correct the object before we send it to the serializer + Fixup func(runtime.Object) runtime.Object TimeoutFactory TimeoutFactory } @@ -205,8 +230,7 @@ func (s *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - obj := event.Object - s.Fixup(obj) + obj := s.Fixup(event.Object) if err := s.EmbeddedEncoder.Encode(obj, buf); err != nil { // unexpected error utilruntime.HandleError(fmt.Errorf("unable to encode watch object %T: %v", obj, err)) @@ -272,8 +296,7 @@ func (s *WatchServer) HandleWS(ws *websocket.Conn) { // End of results. return } - obj := event.Object - s.Fixup(obj) + obj := s.Fixup(event.Object) if err := s.EmbeddedEncoder.Encode(obj, buf); err != nil { // unexpected error utilruntime.HandleError(fmt.Errorf("unable to encode watch object %T: %v", obj, err)) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/watch_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/watch_test.go index 81d141fa753..5d8789a4686 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/watch_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/watch_test.go @@ -586,7 +586,7 @@ func TestWatchHTTPErrors(t *testing.T) { Encoder: newCodec, EmbeddedEncoder: newCodec, - Fixup: func(obj runtime.Object) {}, + Fixup: func(obj runtime.Object) runtime.Object { return obj }, TimeoutFactory: &fakeTimeoutFactory{timeoutCh, done}, } @@ -646,7 +646,7 @@ func TestWatchHTTPDynamicClientErrors(t *testing.T) { Encoder: newCodec, EmbeddedEncoder: newCodec, - Fixup: func(obj runtime.Object) {}, + Fixup: func(obj runtime.Object) runtime.Object { return obj }, TimeoutFactory: &fakeTimeoutFactory{timeoutCh, done}, } @@ -708,7 +708,7 @@ func TestWatchHTTPTimeout(t *testing.T) { Encoder: newCodec, EmbeddedEncoder: newCodec, - Fixup: func(obj runtime.Object) {}, + Fixup: func(obj runtime.Object) runtime.Object { return obj }, TimeoutFactory: &fakeTimeoutFactory{timeoutCh, done}, } diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go index f29f4325b6b..6518968c418 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go @@ -18,6 +18,7 @@ package resttest import ( "context" + "encoding/json" "fmt" "reflect" "strings" @@ -1460,7 +1461,7 @@ func (t *Tester) testListTableConversion(obj runtime.Object, assignFn AssignFunc } columns := table.ColumnDefinitions if len(columns) == 0 { - t.Errorf("unexpected number of columns: %v", len(columns)) + t.Fatalf("unexpected number of columns: %v\n%#v", len(columns), columns) } if !strings.EqualFold(columns[0].Name, "Name") || columns[0].Type != "string" || columns[0].Format != "name" { t.Errorf("expect column 0 to be the name column: %#v", columns[0]) @@ -1505,8 +1506,11 @@ func (t *Tester) testListTableConversion(obj runtime.Object, assignFn AssignFunc } } if len(row.Cells) != len(table.ColumnDefinitions) { + t.Fatalf("unmatched row length on row %d: %#v", i, row.Cells) } } + data, _ := json.MarshalIndent(table, "", " ") + t.Logf("%s", string(data)) } // ============================================================================= diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go index bfcdcf58b13..d954ca5240b 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go @@ -73,9 +73,11 @@ func (c defaultTableConvertor) ConvertToTable(ctx context.Context, object runtim table.SelfLink = m.GetSelfLink() } } - table.ColumnDefinitions = []metav1beta1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name", Description: swaggerMetadataDescriptions["name"]}, - {Name: "Created At", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"]}, + if opt, ok := tableOptions.(*metav1beta1.TableOptions); !ok || !opt.NoHeaders { + table.ColumnDefinitions = []metav1beta1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: swaggerMetadataDescriptions["name"]}, + {Name: "Created At", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"]}, + } } return &table, nil } diff --git a/staging/src/k8s.io/client-go/dynamic/simple.go b/staging/src/k8s.io/client-go/dynamic/simple.go index 852f0c5120a..65c96dcc182 100644 --- a/staging/src/k8s.io/client-go/dynamic/simple.go +++ b/staging/src/k8s.io/client-go/dynamic/simple.go @@ -36,6 +36,19 @@ type dynamicClient struct { var _ Interface = &dynamicClient{} +// ConfigFor returns a copy of the provided config with the +// appropriate dynamic client defaults set. +func ConfigFor(inConfig *rest.Config) *rest.Config { + config := rest.CopyConfig(inConfig) + config.AcceptContentTypes = "application/json" + config.ContentType = "application/json" + config.NegotiatedSerializer = basicNegotiatedSerializer{} // this gets used for discovery and error handling types + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + return config +} + // NewForConfigOrDie creates a new Interface for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) Interface { @@ -46,17 +59,12 @@ func NewForConfigOrDie(c *rest.Config) Interface { return ret } +// NewForConfig creates a new dynamic client or returns an error. func NewForConfig(inConfig *rest.Config) (Interface, error) { - config := rest.CopyConfig(inConfig) + config := ConfigFor(inConfig) // for serializing the options config.GroupVersion = &schema.GroupVersion{} config.APIPath = "/if-you-see-this-search-for-the-break" - config.AcceptContentTypes = "application/json" - config.ContentType = "application/json" - config.NegotiatedSerializer = basicNegotiatedSerializer{} // this gets used for discovery and error handling types - if config.UserAgent == "" { - config.UserAgent = rest.DefaultKubernetesUserAgent() - } restClient, err := rest.RESTClientFor(config) if err != nil { diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index 6d5f49d7afb..be8d8987aef 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -686,6 +686,10 @@ "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apimachinery/pkg/conversion", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD index a931e6fc793..4cca019e819 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD @@ -54,6 +54,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_apis.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_apis.go index 82543abaa61..0516df6a8e0 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_apis.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_apis.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" apiregistrationapi "k8s.io/kube-aggregator/pkg/apis/apiregistration" @@ -81,7 +82,7 @@ func (r *apisHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } } - responsewriters.WriteObjectNegotiated(r.codecs, schema.GroupVersion{}, w, req, http.StatusOK, discoveryGroupList) + responsewriters.WriteObjectNegotiated(r.codecs, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, w, req, http.StatusOK, discoveryGroupList) } // convertToDiscoveryAPIGroup takes apiservices in a single group and returns a discovery compatible object. @@ -152,5 +153,5 @@ func (r *apiGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { http.Error(w, "", http.StatusNotFound) return } - responsewriters.WriteObjectNegotiated(r.codecs, schema.GroupVersion{}, w, req, http.StatusOK, discoveryGroup) + responsewriters.WriteObjectNegotiated(r.codecs, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, w, req, http.StatusOK, discoveryGroup) } diff --git a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json index 363f0949966..d6755129da3 100644 --- a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json @@ -670,6 +670,10 @@ "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1/validation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apimachinery/pkg/conversion", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/test/integration/apiserver/BUILD b/test/integration/apiserver/BUILD index 55f59599097..3ba0243ba52 100644 --- a/test/integration/apiserver/BUILD +++ b/test/integration/apiserver/BUILD @@ -39,18 +39,27 @@ go_test( "//staging/src/k8s.io/api/scheduling/v1beta1:go_default_library", "//staging/src/k8s.io/api/settings/v1alpha1:go_default_library", "//staging/src/k8s.io/api/storage/v1alpha1:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/protobuf:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/streaming:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/client-go/discovery/cached/disk:go_default_library", + "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library", diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 0086dadddd5..1b212a8f332 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -19,26 +19,43 @@ package apiserver import ( "bytes" "context" + "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" "path" "reflect" + "strconv" + "strings" "testing" apps "k8s.io/api/apps/v1" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/test/integration/fixtures" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" 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/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/protobuf" + "k8s.io/apimachinery/pkg/runtime/serializer/streaming" + "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" + "k8s.io/client-go/dynamic" clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/pager" + "k8s.io/klog" "k8s.io/kubernetes/pkg/master" "k8s.io/kubernetes/test/integration/framework" @@ -322,3 +339,456 @@ func TestNameInFieldSelector(t *testing.T) { } } } + +func TestTransformOnWatch(t *testing.T) { + tearDown, config, _, err := fixtures.StartDefaultServer(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + s, clientset, closeFn := setup(t) + defer closeFn() + + apiExtensionClient, err := apiextensionsclient.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + fooCRD := &apiextensionsv1beta1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foos.cr.bar.com", + }, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: "cr.bar.com", + Version: "v1", + Scope: apiextensionsv1beta1.NamespaceScoped, + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: "foos", + Kind: "Foo", + }, + }, + } + fooCRD, err = fixtures.CreateNewCustomResourceDefinition(fooCRD, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + crdGVR := schema.GroupVersionResource{Group: fooCRD.Spec.Group, Version: fooCRD.Spec.Version, Resource: "foos"} + crclient := dynamicClient.Resource(crdGVR).Namespace("default") + + testcases := []struct { + name string + accept string + includeObject metav1beta1.IncludeObjectPolicy + object func(*testing.T) (metav1.Object, string, string) + wantErr func(*testing.T, error) + wantBody func(*testing.T, io.Reader) + }{ + { + name: "verify columns on cluster scoped resources", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + return &metav1.ObjectMeta{Name: "default", Namespace: ""}, "", "namespaces" + }, + wantBody: func(t *testing.T, w io.Reader) { + expectTableWatchEvents(t, 1, 3, metav1beta1.IncludeMetadata, json.NewDecoder(w)) + }, + }, + { + name: "verify columns on CRDs in json", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + cr, err := crclient.Create(&unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-1"}}}, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unable to create cr: %v", err) + } + if _, err := crclient.Patch("test-1", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil { + t.Fatalf("unable to patch cr: %v", err) + } + return cr, crdGVR.Group, "foos" + }, + wantBody: func(t *testing.T, w io.Reader) { + expectTableWatchEvents(t, 2, 2, metav1beta1.IncludeMetadata, json.NewDecoder(w)) + }, + }, + { + name: "verify columns on CRDs in json;stream=watch", + accept: "application/json;stream=watch;as=Table;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + cr, err := crclient.Create(&unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-2"}}}, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unable to create cr: %v", err) + } + if _, err := crclient.Patch("test-2", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil { + t.Fatalf("unable to patch cr: %v", err) + } + return cr, crdGVR.Group, "foos" + }, + wantBody: func(t *testing.T, w io.Reader) { + expectTableWatchEvents(t, 2, 2, metav1beta1.IncludeMetadata, json.NewDecoder(w)) + }, + }, + { + name: "verify columns on CRDs in yaml", + accept: "application/yaml;as=Table;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + cr, err := crclient.Create(&unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-3"}}}, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unable to create cr: %v", err) + } + if _, err := crclient.Patch("test-3", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil { + t.Fatalf("unable to patch cr: %v", err) + } + return cr, crdGVR.Group, "foos" + }, + wantErr: func(t *testing.T, err error) { + if !apierrors.IsNotAcceptable(err) { + t.Fatal(err) + } + // TODO: this should be a more specific error + if err.Error() != "only the following media types are accepted: application/json;stream=watch" { + t.Fatal(err) + } + }, + }, + { + name: "verify columns on services", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + ns := "default" + svc, err := clientset.CoreV1().Services(ns).Create(&v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-1"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}) + if err != nil { + t.Fatalf("unable to create service: %v", err) + } + if _, err := clientset.CoreV1().Services(ns).Patch(svc.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`)); err != nil { + t.Fatalf("unable to update service: %v", err) + } + return svc, "", "services" + }, + wantBody: func(t *testing.T, w io.Reader) { + expectTableWatchEvents(t, 2, 7, metav1beta1.IncludeMetadata, json.NewDecoder(w)) + }, + }, + { + name: "verify columns on services with no object", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1", + includeObject: metav1beta1.IncludeNone, + object: func(t *testing.T) (metav1.Object, string, string) { + ns := "default" + obj, err := clientset.CoreV1().Services(ns).Create(&v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-2"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + if _, err := clientset.CoreV1().Services(ns).Patch(obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`)); err != nil { + t.Fatalf("unable to update object: %v", err) + } + return obj, "", "services" + }, + wantBody: func(t *testing.T, w io.Reader) { + expectTableWatchEvents(t, 2, 7, metav1beta1.IncludeNone, json.NewDecoder(w)) + }, + }, + { + name: "verify columns on services with full object", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1", + includeObject: metav1beta1.IncludeObject, + object: func(t *testing.T) (metav1.Object, string, string) { + ns := "default" + obj, err := clientset.CoreV1().Services(ns).Create(&v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-3"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + if _, err := clientset.CoreV1().Services(ns).Patch(obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`)); err != nil { + t.Fatalf("unable to update object: %v", err) + } + return obj, "", "services" + }, + wantBody: func(t *testing.T, w io.Reader) { + objects := expectTableWatchEvents(t, 2, 7, metav1beta1.IncludeObject, json.NewDecoder(w)) + var svc v1.Service + if err := json.Unmarshal(objects[1], &svc); err != nil { + t.Fatal(err) + } + if svc.Annotations["test"] != "1" || svc.Spec.Ports[0].Port != 1000 { + t.Fatalf("unexpected object: %#v", svc) + } + }, + }, + { + name: "verify partial metadata object on config maps", + accept: "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + ns := "default" + obj, err := clientset.CoreV1().ConfigMaps(ns).Create(&v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "test-1", Annotations: map[string]string{"test": "0"}}}) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + if _, err := clientset.CoreV1().ConfigMaps(ns).Patch(obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`)); err != nil { + t.Fatalf("unable to update object: %v", err) + } + return obj, "", "configmaps" + }, + wantBody: func(t *testing.T, w io.Reader) { + expectPartialObjectMetaEvents(t, json.NewDecoder(w), "0", "1") + }, + }, + { + name: "verify partial metadata object on config maps in protobuf", + accept: "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + ns := "default" + obj, err := clientset.CoreV1().ConfigMaps(ns).Create(&v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "test-2", Annotations: map[string]string{"test": "0"}}}) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + if _, err := clientset.CoreV1().ConfigMaps(ns).Patch(obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`)); err != nil { + t.Fatalf("unable to update object: %v", err) + } + return obj, "", "configmaps" + }, + wantBody: func(t *testing.T, w io.Reader) { + expectPartialObjectMetaEventsProtobuf(t, w, "0", "1") + }, + }, + { + name: "verify error on unsupported mimetype protobuf for table conversion", + accept: "application/vnd.kubernetes.protobuf;as=Table;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services" + }, + wantErr: func(t *testing.T, err error) { + if !apierrors.IsNotAcceptable(err) { + t.Fatal(err) + } + // TODO: this should be a more specific error + if err.Error() != "only the following media types are accepted: application/json, application/yaml, application/vnd.kubernetes.protobuf" { + t.Fatal(err) + } + }, + }, + { + name: "verify error on invalid mimetype - bad version", + accept: "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1", + object: func(t *testing.T) (metav1.Object, string, string) { + return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services" + }, + wantErr: func(t *testing.T, err error) { + if !apierrors.IsNotAcceptable(err) { + t.Fatal(err) + } + }, + }, + { + name: "verify error on invalid mimetype - bad group", + accept: "application/json;as=PartialObjectMetadata;g=k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services" + }, + wantErr: func(t *testing.T, err error) { + if !apierrors.IsNotAcceptable(err) { + t.Fatal(err) + } + }, + }, + { + name: "verify error on invalid mimetype - bad kind", + accept: "application/json;as=PartialObject;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services" + }, + wantErr: func(t *testing.T, err error) { + if !apierrors.IsNotAcceptable(err) { + t.Fatal(err) + } + }, + }, + { + name: "verify error on invalid mimetype - missing kind", + accept: "application/json;g=meta.k8s.io;v=v1beta1", + object: func(t *testing.T) (metav1.Object, string, string) { + return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services" + }, + wantErr: func(t *testing.T, err error) { + if !apierrors.IsNotAcceptable(err) { + t.Fatal(err) + } + }, + }, + { + name: "verify error on invalid transform parameter", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1", + includeObject: metav1beta1.IncludeObjectPolicy("unrecognized"), + object: func(t *testing.T) (metav1.Object, string, string) { + return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services" + }, + wantErr: func(t *testing.T, err error) { + if !apierrors.IsBadRequest(err) || !strings.Contains(err.Error(), `Invalid value: "unrecognized": must be 'Metadata', 'Object', 'None', or empty`) { + t.Fatal(err) + } + }, + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(tc.name, func(t *testing.T) { + obj, group, resource := tc.object(t) + + cfg := dynamic.ConfigFor(config) + if len(group) == 0 { + cfg = dynamic.ConfigFor(&restclient.Config{Host: s.URL}) + cfg.APIPath = "/api" + } else { + cfg.APIPath = "/apis" + } + cfg.GroupVersion = &schema.GroupVersion{Group: group, Version: "v1"} + + client, err := restclient.RESTClientFor(cfg) + if err != nil { + t.Fatal(err) + } + + rv, _ := strconv.Atoi(obj.GetResourceVersion()) + if rv < 1 { + rv = 1 + } + + w, err := client.Get(). + Resource(resource).NamespaceIfScoped(obj.GetNamespace(), len(obj.GetNamespace()) > 0). + SetHeader("Accept", tc.accept). + VersionedParams(&metav1.ListOptions{ + ResourceVersion: strconv.Itoa(rv - 1), + Watch: true, + FieldSelector: fields.OneTermEqualSelector("metadata.name", obj.GetName()).String(), + }, metav1.ParameterCodec). + Param("includeObject", string(tc.includeObject)). + Stream() + if (tc.wantErr != nil) != (err != nil) { + t.Fatalf("unexpected error: %v", err) + } + if tc.wantErr != nil { + tc.wantErr(t, err) + return + } + if err != nil { + t.Fatal(err) + } + defer w.Close() + tc.wantBody(t, w) + }) + } +} + +func expectTableWatchEvents(t *testing.T, count, columns int, policy metav1beta1.IncludeObjectPolicy, d *json.Decoder) [][]byte { + t.Helper() + + var objects [][]byte + + for i := 0; i < count; i++ { + var evt metav1.WatchEvent + if err := d.Decode(&evt); err != nil { + t.Fatal(err) + } + var table metav1beta1.Table + if err := json.Unmarshal(evt.Object.Raw, &table); err != nil { + t.Fatal(err) + } + if i == 0 { + if len(table.ColumnDefinitions) != columns { + t.Fatalf("Got unexpected columns on first watch event: %d vs %#v", columns, table.ColumnDefinitions) + } + } else { + if len(table.ColumnDefinitions) != 0 { + t.Fatalf("Expected no columns on second watch event: %#v", table.ColumnDefinitions) + } + } + if len(table.Rows) != 1 { + t.Fatalf("Invalid rows: %#v", table.Rows) + } + row := table.Rows[0] + if len(row.Cells) != columns { + t.Fatalf("Invalid row width: %#v", row.Cells) + } + switch policy { + case metav1beta1.IncludeMetadata: + var meta metav1beta1.PartialObjectMetadata + if err := json.Unmarshal(row.Object.Raw, &meta); err != nil { + t.Fatalf("expected partial object: %v", err) + } + partialObj := metav1.TypeMeta{Kind: "PartialObjectMetadata", APIVersion: "meta.k8s.io/v1beta1"} + if meta.TypeMeta != partialObj { + t.Fatalf("expected partial object: %#v", meta) + } + case metav1beta1.IncludeNone: + if len(row.Object.Raw) != 0 { + t.Fatalf("Expected no object: %s", string(row.Object.Raw)) + } + case metav1beta1.IncludeObject: + if len(row.Object.Raw) == 0 { + t.Fatalf("Expected object: %s", string(row.Object.Raw)) + } + objects = append(objects, row.Object.Raw) + } + } + return objects +} + +func expectPartialObjectMetaEvents(t *testing.T, d *json.Decoder, values ...string) { + t.Helper() + + for i, value := range values { + var evt metav1.WatchEvent + if err := d.Decode(&evt); err != nil { + t.Fatal(err) + } + var meta metav1beta1.PartialObjectMetadata + if err := json.Unmarshal(evt.Object.Raw, &meta); err != nil { + t.Fatal(err) + } + typeMeta := metav1.TypeMeta{Kind: "PartialObjectMetadata", APIVersion: "meta.k8s.io/v1beta1"} + if meta.TypeMeta != typeMeta { + t.Fatalf("expected partial object: %#v", meta) + } + if meta.Annotations["test"] != value { + t.Fatalf("expected event %d to have value %q instead of %q", i+1, value, meta.Annotations["test"]) + } + } +} + +func expectPartialObjectMetaEventsProtobuf(t *testing.T, r io.Reader, values ...string) { + scheme := runtime.NewScheme() + metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + rs := protobuf.NewRawSerializer(scheme, scheme, "application/vnd.kubernetes.protobuf") + d := streaming.NewDecoder( + protobuf.LengthDelimitedFramer.NewFrameReader(ioutil.NopCloser(r)), + rs, + ) + ds := metainternalversion.Codecs.UniversalDeserializer() + + for i, value := range values { + var evt metav1.WatchEvent + if _, _, err := d.Decode(nil, &evt); err != nil { + t.Fatal(err) + } + obj, gvk, err := ds.Decode(evt.Object.Raw, nil, nil) + if err != nil { + t.Fatal(err) + } + meta, ok := obj.(*metav1beta1.PartialObjectMetadata) + if !ok { + t.Fatalf("unexpected watch object %T", obj) + } + expected := &schema.GroupVersionKind{Kind: "PartialObjectMetadata", Version: "v1beta1", Group: "meta.k8s.io"} + if !reflect.DeepEqual(expected, gvk) { + t.Fatalf("expected partial object: %#v", meta) + } + if meta.Annotations["test"] != value { + t.Fatalf("expected event %d to have value %q instead of %q", i+1, value, meta.Annotations["test"]) + } + } +}