diff --git a/hack/lib/util.sh b/hack/lib/util.sh index 116e9aab36..e5b20f6184 100644 --- a/hack/lib/util.sh +++ b/hack/lib/util.sh @@ -43,6 +43,16 @@ kube::util::wait_for_url() { return 1 } +# Create a temp dir that'll be deleted at the end of this bash session. +# +# Vars set: +# KUBE_TEMP +kube::util::ensure-temp-dir() { + if [[ -z ${KUBE_TEMP-} ]]; then + KUBE_TEMP=$(mktemp -d -t kubernetes.XXXXXX) + fi +} + # This figures out the host platform without relying on golang. We need this as # we don't want a golang install to be a prerequisite to building yet we need # this info to figure out where the final binaries are placed. diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 800099640c..08b235f5d1 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -33,12 +33,14 @@ function cleanup() [[ -n ${PROXY_PID-} ]] && kill ${PROXY_PID} 1>&2 2>/dev/null kube::etcd::cleanup + rm -rf "${KUBE_TEMP}" kube::log::status "Clean up complete" } trap cleanup EXIT SIGINT +kube::util::ensure-temp-dir kube::etcd::start ETCD_HOST=${ETCD_HOST:-127.0.0.1} @@ -533,6 +535,7 @@ __EOF__ kube::test::describe_object_assert nodes "127.0.0.1" "Name:" "Labels:" "CreationTimestamp:" "Conditions:" "Addresses:" "Capacity:" "Pods:" + ########### # Minions # ########### @@ -548,6 +551,7 @@ __EOF__ kube::test::describe_object_assert minions "127.0.0.1" "Name:" "Conditions:" "Addresses:" "Capacity:" "Pods:" fi + ##################### # Retrieve multiple # ##################### @@ -555,6 +559,20 @@ __EOF__ kube::log::status "Testing kubectl(${version}:multiget)" kube::test::get_object_assert 'nodes/127.0.0.1 service/kubernetes' "{{range.items}}{{.$id_field}}:{{end}}" '127.0.0.1:kubernetes:' + + ########### + # Swagger # + ########### + + if [[ -n "${version}" ]]; then + # Verify schema + file="${KUBE_TEMP}/schema-${version}.json" + curl -s "http://127.0.0.1:${API_PORT}/swaggerapi/api/${version}" > "${file}" + [[ "$(grep "list of returned" "${file}")" ]] + [[ "$(grep "list of pods" "${file}")" ]] + [[ "$(grep "watch for changes to the described resources" "${file}")" ]] + fi + kube::test::clear_all done diff --git a/pkg/api/conversion.go b/pkg/api/conversion.go index 49e3ff86f7..89bfdefff5 100644 --- a/pkg/api/conversion.go +++ b/pkg/api/conversion.go @@ -19,6 +19,8 @@ package api import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -28,12 +30,48 @@ import ( var Codec = runtime.CodecFor(Scheme, "") func init() { + Scheme.AddDefaultingFuncs( + func(obj *ListOptions) { + obj.LabelSelector = labels.Everything() + obj.FieldSelector = fields.Everything() + }, + ) Scheme.AddConversionFuncs( func(in *util.Time, out *util.Time, s conversion.Scope) error { // Cannot deep copy these, because time.Time has unexported fields. *out = *in return nil }, + func(in *string, out *labels.Selector, s conversion.Scope) error { + selector, err := labels.Parse(*in) + if err != nil { + return err + } + *out = selector + return nil + }, + func(in *string, out *fields.Selector, s conversion.Scope) error { + selector, err := fields.ParseSelector(*in) + if err != nil { + return err + } + *out = selector + return nil + }, + func(in *labels.Selector, out *string, s conversion.Scope) error { + if *in == nil { + return nil + } + *out = (*in).String() + return nil + }, + func(in *fields.Selector, out *string, s conversion.Scope) error { + if *in == nil { + return nil + } + *out = (*in).String() + return nil + }, func(in *resource.Quantity, out *resource.Quantity, s conversion.Scope) error { // Cannot deep copy these, because inf.Dec has unexported fields. *out = *in.Copy() diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index b905726b7d..06e3e785f6 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -21,6 +21,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/davecgh/go-spew/spew" @@ -63,6 +65,12 @@ var Semantic = conversion.EqualitiesOrDie( func(a, b util.Time) bool { return a.UTC() == b.UTC() }, + func(a, b labels.Selector) bool { + return a.String() == b.String() + }, + func(a, b fields.Selector) bool { + return a.String() == b.String() + }, ) var standardResources = util.NewStringSet( diff --git a/pkg/api/meta/restmapper.go b/pkg/api/meta/restmapper.go index 1059686912..fb0480298f 100644 --- a/pkg/api/meta/restmapper.go +++ b/pkg/api/meta/restmapper.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: move everything in this file to pkg/api/rest package meta import ( diff --git a/pkg/api/meta/restmapper_test.go b/pkg/api/meta/restmapper_test.go index d849fdd153..726e783472 100644 --- a/pkg/api/meta/restmapper_test.go +++ b/pkg/api/meta/restmapper_test.go @@ -38,10 +38,18 @@ func (fakeCodec) DecodeInto([]byte, runtime.Object) error { type fakeConvertor struct{} +func (fakeConvertor) Convert(in, out interface{}) error { + return nil +} + func (fakeConvertor) ConvertToVersion(in runtime.Object, _ string) (runtime.Object, error) { return in, nil } +func (fakeConvertor) ConvertFieldLabel(version, kind, label, value string) (string, string, error) { + return label, value, nil +} + var validCodec = fakeCodec{} var validAccessor = resourceAccessor{} var validConvertor = fakeConvertor{} diff --git a/pkg/api/register.go b/pkg/api/register.go index bb984859c7..64d9ee529e 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -52,11 +52,12 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, - &DeleteOptions{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, &PersistentVolumeClaimList{}, + &DeleteOptions{}, + &ListOptions{}, ) // Legacy names are supported Scheme.AddKnownTypeWithName("", "Minion", &Node{}) @@ -90,8 +91,9 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} -func (*DeleteOptions) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} +func (*ListOptions) IsAnAPIObject() {} diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index af7f5d30ff..d8c02773de 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -129,7 +129,7 @@ func TestList(t *testing.T) { } var nonRoundTrippableTypes = util.NewStringSet("ContainerManifest", "ContainerManifestList") -var nonInternalRoundTrippableTypes = util.NewStringSet("List") +var nonInternalRoundTrippableTypes = util.NewStringSet("List", "ListOptions") func TestRoundTripTypes(t *testing.T) { // api.Scheme.Log(t) diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 5f8421d6ee..85a2d27eaf 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -23,6 +23,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -87,6 +89,11 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) j.SelfLink = c.RandString() }, + func(j *api.ListOptions, c fuzz.Continue) { + // TODO: add some parsing + j.LabelSelector, _ = labels.Parse("a=b") + j.FieldSelector, _ = fields.ParseSelector("a=b") + }, func(j *api.PodPhase, c fuzz.Continue) { statuses := []api.PodPhase{api.PodPending, api.PodRunning, api.PodFailed, api.PodUnknown} *j = statuses[c.Rand.Intn(len(statuses))] diff --git a/pkg/api/types.go b/pkg/api/types.go index fc802f1002..febcc710e9 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -18,6 +18,8 @@ package api import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -1201,6 +1203,21 @@ type DeleteOptions struct { GracePeriodSeconds *int64 `json:"gracePeriodSeconds"` } +// ListOptions is the query options to a standard REST list call, and has future support for +// watch calls. +type ListOptions struct { + TypeMeta `json:",inline"` + + // A selector based on labels + LabelSelector labels.Selector + // A selector based on fields + FieldSelector fields.Selector + // If true, watch for changes to this list + Watch bool + // The resource version to watch (no effect on list yet) + ResourceVersion string +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. diff --git a/pkg/api/unversioned.go b/pkg/api/unversioned.go index 5792df4c19..9dc19a1247 100644 --- a/pkg/api/unversioned.go +++ b/pkg/api/unversioned.go @@ -40,18 +40,20 @@ func PreV1Beta3(version string) bool { return version == "v1beta1" || version == "v1beta2" } +// TODO: remove me when watch is refactored func LabelSelectorQueryParam(version string) string { if PreV1Beta3(version) { return "labels" } - return "label-selector" + return "labelSelector" } +// TODO: remove me when watch is refactored func FieldSelectorQueryParam(version string) string { if PreV1Beta3(version) { return "fields" } - return "field-selector" + return "fieldSelector" } // String returns available api versions as a human-friendly version string. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 1fb7b3d754..f59f0a04df 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -1493,7 +1493,7 @@ func init() { } // Add field conversion funcs. - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "pods", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Pod", func(label, value string) (string, string, error) { switch label { case "name": @@ -1513,7 +1513,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "replicationControllers", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "ReplicationController", func(label, value string) (string, string, error) { switch label { case "name": @@ -1528,7 +1528,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "events", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Event", func(label, value string) (string, string, error) { switch label { case "involvedObject.kind", @@ -1550,7 +1550,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Namespace", func(label, value string) (string, string, error) { switch label { case "status.phase": diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index 456a634a24..a0b9fb3887 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -59,11 +59,12 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, - &DeleteOptions{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, &PersistentVolumeClaimList{}, + &DeleteOptions{}, + &ListOptions{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) @@ -97,8 +98,9 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} -func (*DeleteOptions) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} +func (*ListOptions) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 7de5e55489..65a23f7596 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -1034,6 +1034,20 @@ type DeleteOptions struct { GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` } +// ListOptions is the query options to a standard REST list call +type ListOptions struct { + TypeMeta `json:",inline"` + + // A selector based on labels + LabelSelector string `json:"labels" description:"a selector to restrict the list of returned objects by their labels; defaults to everything"` + // A selector based on fields + FieldSelector string `json:"fields" description:"a selector to restrict the list of returned objects by their fields; defaults to everything"` + // If true, watch for changes to the selected resources + Watch bool `json:"watch" description:"watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion"` + // The desired resource version to watch + ResourceVersion string `json:"resourceVersion" description:"when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history"` +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index a90c7d1d55..3149a21a3b 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -1419,7 +1419,7 @@ func init() { } // Add field conversion funcs. - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "pods", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "Pod", func(label, value string) (string, string, error) { switch label { case "name": @@ -1439,7 +1439,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "replicationControllers", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "ReplicationController", func(label, value string) (string, string, error) { switch label { case "name": @@ -1454,7 +1454,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "events", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "Event", func(label, value string) (string, string, error) { switch label { case "involvedObject.kind", @@ -1476,7 +1476,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Namespace", func(label, value string) (string, string, error) { switch label { case "status.phase": diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index 21be3ff2c3..ad69988caf 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -59,11 +59,12 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, - &DeleteOptions{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, &PersistentVolumeClaimList{}, + &DeleteOptions{}, + &ListOptions{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) @@ -102,3 +103,4 @@ func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*DeleteOptions) IsAnAPIObject() {} +func (*ListOptions) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index e3d8a15209..55088c4f80 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -1048,6 +1048,20 @@ type DeleteOptions struct { GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` } +// ListOptions is the query options to a standard REST list call +type ListOptions struct { + TypeMeta `json:",inline"` + + // A selector based on labels + LabelSelector string `json:"labels" description:"a selector to restrict the list of returned objects by their labels; defaults to everything"` + // A selector based on fields + FieldSelector string `json:"fields" description:"a selector to restrict the list of returned objects by their fields; defaults to everything"` + // If true, watch for changes to the selected resources + Watch bool `json:"watch" description:"watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion"` + // The desired resource version to watch + ResourceVersion string `json:"resourceVersion" description:"when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history"` +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. diff --git a/pkg/api/v1beta3/conversion.go b/pkg/api/v1beta3/conversion.go index 50a025eb93..1f1b38d611 100644 --- a/pkg/api/v1beta3/conversion.go +++ b/pkg/api/v1beta3/conversion.go @@ -24,7 +24,7 @@ import ( func init() { // Add field conversion funcs. - err := newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "pods", + err := newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "Pod", func(label, value string) (string, string, error) { switch label { case "name", @@ -39,7 +39,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "replicationControllers", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "ReplicationController", func(label, value string) (string, string, error) { switch label { case "name": @@ -54,7 +54,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "events", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "Event", func(label, value string) (string, string, error) { switch label { case "involvedObject.kind", @@ -75,7 +75,7 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } - err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Namespace", func(label, value string) (string, string, error) { switch label { case "status.phase": diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index ae74d2f8ed..0d9c7a7a99 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -53,11 +53,12 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, - &DeleteOptions{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, &PersistentVolumeClaimList{}, + &DeleteOptions{}, + &ListOptions{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) @@ -96,3 +97,4 @@ func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*DeleteOptions) IsAnAPIObject() {} +func (*ListOptions) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index f97bad0f5e..d7837e3f77 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -1190,6 +1190,20 @@ type DeleteOptions struct { GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` } +// ListOptions is the query options to a standard REST list call +type ListOptions struct { + TypeMeta `json:",inline"` + + // A selector based on labels + LabelSelector string `json:"labelSelector" description:"a selector to restrict the list of returned objects by their labels; defaults to everything"` + // A selector based on fields + FieldSelector string `json:"fieldSelector" description:"a selector to restrict the list of returned objects by their fields; defaults to everything"` + // If true, watch for changes to the selected resources + Watch bool `json:"watch" description:"watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion"` + // The desired resource version to watch + ResourceVersion string `json:"resourceVersion" description:"when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history"` +} + // Status is a return value for calls that don't return other objects. type Status struct { TypeMeta `json:",inline"` diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 4dda0319b8..e790409941 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -29,7 +29,9 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" "github.com/emicklei/go-restful" ) @@ -58,13 +60,6 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { // Create the WebService. ws = a.newWebService() - // Initialize the custom handlers. - watchHandler := (&WatchHandler{ - storage: a.group.Storage, - codec: a.group.Codec, - linker: a.group.Linker, - info: a.info, - }) redirectHandler := (&RedirectHandler{a.group.Storage, a.group.Codec, a.group.Context, a.info}) proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info}) @@ -77,7 +72,7 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { } sort.Strings(paths) for _, path := range paths { - if err := a.registerResourceHandlers(path, a.group.Storage[path], ws, watchHandler, redirectHandler, proxyHandler); err != nil { + if err := a.registerResourceHandlers(path, a.group.Storage[path], ws, redirectHandler, proxyHandler); err != nil { errors = append(errors, err) } } @@ -95,10 +90,15 @@ func (a *APIInstaller) newWebService() *restful.WebService { return ws } -func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, watchHandler, redirectHandler, proxyHandler http.Handler) error { +func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, redirectHandler, proxyHandler http.Handler) error { admit := a.group.Admit context := a.group.Context + serverVersion := a.group.ServerVersion + if len(serverVersion) == 0 { + serverVersion = a.group.Version + } + var resource, subresource string switch parts := strings.Split(path, "/"); len(parts) { case 2: @@ -121,17 +121,6 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag } versionedObject := indirectArbitraryPointer(versionedPtr) - var versionedList interface{} - if lister, ok := storage.(rest.Lister); ok { - list := lister.NewList() - _, listKind, err := a.group.Typer.ObjectVersionAndKind(list) - versionedListPtr, err := a.group.Creater.New(a.group.Version, listKind) - if err != nil { - return err - } - versionedList = indirectArbitraryPointer(versionedListPtr) - } - mapping, err := a.group.Mapper.RESTMapping(kind, a.group.Version) if err != nil { return err @@ -145,17 +134,33 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter) updater, isUpdater := storage.(rest.Updater) patcher, isPatcher := storage.(rest.Patcher) - _, isWatcher := storage.(rest.Watcher) + watcher, isWatcher := storage.(rest.Watcher) _, isRedirector := storage.(rest.Redirector) storageMeta, isMetadata := storage.(rest.StorageMetadata) if !isMetadata { storageMeta = defaultStorageMetadata{} } + var versionedList interface{} + if isLister { + list := lister.NewList() + _, listKind, err := a.group.Typer.ObjectVersionAndKind(list) + versionedListPtr, err := a.group.Creater.New(a.group.Version, listKind) + if err != nil { + return err + } + versionedList = indirectArbitraryPointer(versionedListPtr) + } + + versionedListOptions, err := a.group.Creater.New(serverVersion, "ListOptions") + if err != nil { + return err + } + var versionedDeleterObject runtime.Object switch { case isGracefulDeleter: - object, err := a.group.Creater.New(a.group.Version, "DeleteOptions") + object, err := a.group.Creater.New(serverVersion, "DeleteOptions") if err != nil { return err } @@ -288,11 +293,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag // test/integration/auth_test.go is currently the most comprehensive status code test reqScope := RequestScope{ - ContextFunc: ctxFn, - Codec: mapping.Codec, - APIVersion: a.group.Version, - Resource: resource, - Kind: kind, + ContextFunc: ctxFn, + Creater: a.group.Creater, + Convertor: a.group.Convertor, + Codec: mapping.Codec, + APIVersion: a.group.Version, + ServerAPIVersion: serverVersion, + Resource: resource, + Kind: kind, } for _, action := range actions { reqScope.Namer = action.Namer @@ -308,12 +316,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag addParams(route, action.Params) ws.Route(route) case "LIST": // List all resources of a kind. - route := ws.GET(action.Path).To(ListResource(lister, reqScope)). + route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, false)). Filter(m). Doc("list objects of kind " + kind). Operation("list" + kind). Produces("application/json"). Writes(versionedList) + if err := addObjectParams(ws, route, versionedListOptions); err != nil { + return err + } addParams(route, action.Params) ws.Route(route) case "PUT": // Update a resource. @@ -356,22 +367,30 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag } addParams(route, action.Params) ws.Route(route) + // TODO: deprecated case "WATCH": // Watch a resource. - route := ws.GET(action.Path).To(routeFunction(watchHandler)). + route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, true)). Filter(m). - Doc("watch a particular " + kind). + Doc("watch changes to an object of kind " + kind). Operation("watch" + kind). Produces("application/json"). - Writes(versionedObject) + Writes(watchjson.NewWatchEvent()) + if err := addObjectParams(ws, route, versionedListOptions); err != nil { + return err + } addParams(route, action.Params) ws.Route(route) + // TODO: deprecated case "WATCHLIST": // Watch all resources of a kind. - route := ws.GET(action.Path).To(routeFunction(watchHandler)). + route := ws.GET(action.Path).To(ListResource(lister, watcher, reqScope, true)). Filter(m). - Doc("watch a list of " + kind). + Doc("watch individual changes to a list of " + kind). Operation("watch" + kind + "list"). Produces("application/json"). - Writes(versionedList) + Writes(watchjson.NewWatchEvent()) + if err := addObjectParams(ws, route, versionedListOptions); err != nil { + return err + } addParams(route, action.Params) ws.Route(route) case "REDIRECT": // Get the redirect URL for a resource. @@ -651,6 +670,45 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) { } } +// addObjectParams converts a runtime.Object into a set of go-restful Param() definitions on the route. +// The object must be a pointer to a struct; only fields at the top level of the struct that are not +// themselves interfaces or structs are used; only fields with a json tag that is non empty (the standard +// Go JSON behavior for omitting a field) become query parameters. The name of the query parameter is +// the JSON field name. If a description struct tag is set on the field, that description is used on the +// query parameter. In essence, it converts a standard JSON top level object into a query param schema. +func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj runtime.Object) error { + sv, err := conversion.EnforcePtr(obj) + if err != nil { + return err + } + st := sv.Type() + switch st.Kind() { + case reflect.Struct: + for i := 0; i < st.NumField(); i++ { + name := st.Field(i).Name + sf, ok := st.FieldByName(name) + if !ok { + continue + } + switch sf.Type.Kind() { + case reflect.Interface, reflect.Struct: + default: + jsonTag := sf.Tag.Get("json") + if len(jsonTag) == 0 { + continue + } + jsonName := strings.SplitN(jsonTag, ",", 2)[0] + if len(jsonName) == 0 { + continue + } + desc := sf.Tag.Get("description") + route.Param(ws.QueryParameter(jsonName, desc).DataType(sf.Type.Name())) + } + } + } + return nil +} + // defaultStorageMetadata provides default answers to rest.StorageMetadata. type defaultStorageMetadata struct{} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index a16b582fcc..c92b9b4ded 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -102,12 +102,19 @@ type APIGroupVersion struct { Root string Version string + // ServerVersion controls the Kubernetes APIVersion used for common objects in the apiserver + // schema like api.Status, api.DeleteOptions, and api.ListOptions. Other implementors may + // define a version "v1beta1" but want to use the Kubernetes "v1beta3" internal objects. If + // empty, defaults to Version. + ServerVersion string + Mapper meta.RESTMapper - Codec runtime.Codec - Typer runtime.ObjectTyper - Creater runtime.ObjectCreater - Linker runtime.SelfLinker + Codec runtime.Codec + Typer runtime.ObjectTyper + Creater runtime.ObjectCreater + Convertor runtime.ObjectConvertor + Linker runtime.SelfLinker Admit admission.Interface Context api.RequestContextMapper @@ -197,7 +204,11 @@ func APIVersionHandler(versions ...string) restful.RouteFunction { } } -// write renders a returned runtime.Object to the response as a stream or an encoded object. +// write 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 write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) { if stream, ok := object.(rest.ResourceStreamer); ok { out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept")) diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index b753ec462f..3476a8cd89 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -37,6 +37,8 @@ import ( apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -53,11 +55,21 @@ func convert(obj runtime.Object) (runtime.Object, error) { return obj, nil } -// This creates a fake API version, similar to api/latest.go +// This creates a fake API version, similar to api/latest.go for a v1beta1 equivalent api. It is distinct +// from the Kubernetes API versions to allow clients to properly distinguish the two. const testVersion = "version" -var versions = []string{testVersion} -var codec = runtime.CodecFor(api.Scheme, testVersion) +// The equivalent of the Kubernetes v1beta3 API. +const testVersion2 = "version2" + +var versions = []string{testVersion, testVersion2} +var legacyCodec = runtime.CodecFor(api.Scheme, testVersion) +var codec = runtime.CodecFor(api.Scheme, testVersion2) + +// these codecs reflect ListOptions/DeleteOptions coming from the serverAPIversion +var versionServerCodec = runtime.CodecFor(api.Scheme, "v1beta1") +var version2ServerCodec = runtime.CodecFor(api.Scheme, "v1beta3") + var accessor = meta.NewAccessor() var versioner runtime.ResourceVersioner = accessor var selfLinker runtime.SelfLinker = accessor @@ -68,6 +80,12 @@ var requestContextMapper api.RequestContextMapper func interfacesFor(version string) (*meta.VersionInterfaces, error) { switch version { case testVersion: + return &meta.VersionInterfaces{ + Codec: legacyCodec, + ObjectConvertor: api.Scheme, + MetadataAccessor: accessor, + }, nil + case testVersion2: return &meta.VersionInterfaces{ Codec: codec, ObjectConvertor: api.Scheme, @@ -96,11 +114,13 @@ func init() { // api.Status is returned in errors // "internal" version - api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, - &api.Status{}) + api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, &api.Status{}, &api.ListOptions{}) // "version" version // TODO: Use versioned api objects? - api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &api.DeleteOptions{}, &api.Status{}) + api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &v1beta1.Status{}) + // "version2" version + // TODO: Use versioned api objects? + api.Scheme.AddKnownTypes(testVersion2, &Simple{}, &SimpleList{}, &v1beta3.Status{}) nsMapper := newMapper() legacyNsMapper := newMapper() @@ -118,6 +138,18 @@ func init() { namespaceMapper = nsMapper admissionControl = admit.NewAlwaysAdmit() requestContextMapper = api.NewRequestContextMapper() + + //mapper.(*meta.DefaultRESTMapper).Add(meta.RESTScopeNamespaceLegacy, "Simple", testVersion, false) + api.Scheme.AddFieldLabelConversionFunc(testVersion, "Simple", + func(label, value string) (string, string, error) { + return label, value, nil + }, + ) + api.Scheme.AddFieldLabelConversionFunc(testVersion2, "Simple", + func(label, value string) (string, string, error) { + return label, value, nil + }, + ) } // defaultAPIServer exposes nested objects for testability. @@ -129,45 +161,61 @@ type defaultAPIServer struct { // uses the default settings func handle(storage map[string]rest.Storage) http.Handler { - return handleInternal(storage, admissionControl, mapper, selfLinker) + return handleInternal(true, storage, admissionControl, selfLinker) +} + +// uses the default settings for a v1beta3 compatible api +func handleNew(storage map[string]rest.Storage) http.Handler { + return handleInternal(false, storage, admissionControl, selfLinker) } // tests with a deny admission controller func handleDeny(storage map[string]rest.Storage) http.Handler { - return handleInternal(storage, deny.NewAlwaysDeny(), mapper, selfLinker) + return handleInternal(true, storage, deny.NewAlwaysDeny(), selfLinker) } // tests using the new namespace scope mechanism func handleNamespaced(storage map[string]rest.Storage) http.Handler { - return handleInternal(storage, admissionControl, namespaceMapper, selfLinker) + return handleInternal(false, storage, admissionControl, selfLinker) } // tests using a custom self linker func handleLinker(storage map[string]rest.Storage, selfLinker runtime.SelfLinker) http.Handler { - return handleInternal(storage, admissionControl, mapper, selfLinker) + return handleInternal(true, storage, admissionControl, selfLinker) } -func handleInternal(storage map[string]rest.Storage, admissionControl admission.Interface, mapper meta.RESTMapper, selfLinker runtime.SelfLinker) http.Handler { +func handleInternal(legacy bool, storage map[string]rest.Storage, admissionControl admission.Interface, selfLinker runtime.SelfLinker) http.Handler { group := &APIGroupVersion{ Storage: storage, - Mapper: mapper, + Root: "/api", - Root: "/api", - Version: testVersion, - - Creater: api.Scheme, - Typer: api.Scheme, - Codec: codec, - Linker: selfLinker, + Creater: api.Scheme, + Convertor: api.Scheme, + Typer: api.Scheme, + Linker: selfLinker, Admit: admissionControl, Context: requestContextMapper, } + if legacy { + group.Version = testVersion + group.ServerVersion = "v1beta1" + group.Codec = legacyCodec + group.Mapper = legacyNamespaceMapper + } else { + group.Version = testVersion2 + group.ServerVersion = "v1beta3" + group.Codec = codec + group.Mapper = namespaceMapper + } + container := restful.NewContainer() container.Router(restful.CurlyRouter{}) mux := container.ServeMux - group.InstallREST(container) + if err := group.InstallREST(container); err != nil { + panic(fmt.Sprintf("unable to install container %s: %v", group.Version, err)) + } ws := new(restful.WebService) InstallSupport(mux, ws) container.Add(ws) @@ -244,6 +292,8 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, f result := &SimpleList{ Items: storage.list, } + storage.requestedLabelSelector = label + storage.requestedFieldSelector = field return result, storage.errors["list"] } @@ -522,15 +572,60 @@ func TestList(t *testing.T) { namespace string selfLink string legacy bool + label string + field string }{ - {"/api/version/simple", "", "/api/version/simple?namespace=", true}, - {"/api/version/simple?namespace=other", "other", "/api/version/simple?namespace=other", true}, + { + url: "/api/version/simple", + namespace: "", + selfLink: "/api/version/simple?namespace=", + legacy: true, + }, + { + url: "/api/version/simple?namespace=other", + namespace: "other", + selfLink: "/api/version/simple?namespace=other", + legacy: true, + }, + { + url: "/api/version/simple?namespace=other&labels=a%3Db&fields=c%3Dd", + namespace: "other", + selfLink: "/api/version/simple?namespace=other", + legacy: true, + label: "a=b", + field: "c=d", + }, // list items across all namespaces - {"/api/version/simple?namespace=", "", "/api/version/simple?namespace=", true}, - {"/api/version/namespaces/default/simple", "default", "/api/version/namespaces/default/simple", false}, - {"/api/version/namespaces/other/simple", "other", "/api/version/namespaces/other/simple", false}, + { + url: "/api/version/simple?namespace=", + namespace: "", + selfLink: "/api/version/simple?namespace=", + legacy: true, + }, + // list items in a namespace, v1beta3+ + { + url: "/api/version2/namespaces/default/simple", + namespace: "default", + selfLink: "/api/version2/namespaces/default/simple", + }, + { + url: "/api/version2/namespaces/other/simple", + namespace: "other", + selfLink: "/api/version2/namespaces/other/simple", + }, + { + url: "/api/version2/namespaces/other/simple?labelSelector=a%3Db&fieldSelector=c%3Dd", + namespace: "other", + selfLink: "/api/version2/namespaces/other/simple", + label: "a=b", + field: "c=d", + }, // list items across all namespaces - {"/api/version/simple", "", "/api/version/simple", false}, + { + url: "/api/version2/simple", + namespace: "", + selfLink: "/api/version2/simple", + }, } for i, testCase := range testCases { storage := map[string]rest.Storage{} @@ -545,7 +640,7 @@ func TestList(t *testing.T) { if testCase.legacy { handler = handleLinker(storage, selfLinker) } else { - handler = handleInternal(storage, admissionControl, namespaceMapper, selfLinker) + handler = handleInternal(false, storage, admissionControl, selfLinker) } server := httptest.NewServer(handler) defer server.Close() @@ -557,6 +652,9 @@ func TestList(t *testing.T) { } if resp.StatusCode != http.StatusOK { t.Errorf("%d: unexpected status: %d, Expected: %d, %#v", i, resp.StatusCode, http.StatusOK, resp) + body, _ := ioutil.ReadAll(resp.Body) + t.Logf("%d: body: %s", string(body)) + continue } // TODO: future, restore get links if !selfLinker.called { @@ -567,6 +665,12 @@ func TestList(t *testing.T) { } else if simpleStorage.actualNamespace != testCase.namespace { t.Errorf("%d: unexpected resource namespace: %s", i, simpleStorage.actualNamespace) } + if simpleStorage.requestedLabelSelector == nil || simpleStorage.requestedLabelSelector.String() != testCase.label { + t.Errorf("%d: unexpected label selector: %v", i, simpleStorage.requestedLabelSelector) + } + if simpleStorage.requestedFieldSelector == nil || simpleStorage.requestedFieldSelector.String() != testCase.field { + t.Errorf("%d: unexpected field selector: %v", i, simpleStorage.requestedFieldSelector) + } } } @@ -821,16 +925,16 @@ func TestGetNamespaceSelfLink(t *testing.T) { } selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/api/version/namespaces/foo/simple/id", + expectedSet: "/api/version2/namespaces/foo/simple/id", name: "id", namespace: "foo", } storage["simple"] = &simpleStorage - handler := handleInternal(storage, admissionControl, namespaceMapper, selfLinker) + handler := handleInternal(false, storage, admissionControl, selfLinker) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/api/version/namespaces/foo/simple/id") + resp, err := http.Get(server.URL + "/api/version2/namespaces/foo/simple/id") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -905,7 +1009,7 @@ func TestDeleteWithOptions(t *testing.T) { item := &api.DeleteOptions{ GracePeriodSeconds: &grace, } - body, err := codec.Encode(item) + body, err := versionServerCodec.Encode(item) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -966,7 +1070,7 @@ func TestLegacyDeleteIgnoresOptions(t *testing.T) { defer server.Close() item := api.NewDeleteOptions(300) - body, err := codec.Encode(item) + body, err := versionServerCodec.Encode(item) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1575,7 +1679,7 @@ func TestCreateInvokesAdmissionControl(t *testing.T) { namespace: "other", expectedSet: "/api/version/foo/bar?namespace=other", } - handler := handleInternal(map[string]rest.Storage{"foo": &storage}, deny.NewAlwaysDeny(), mapper, selfLinker) + handler := handleInternal(true, map[string]rest.Storage{"foo": &storage}, deny.NewAlwaysDeny(), selfLinker) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} diff --git a/pkg/apiserver/proxy_test.go b/pkg/apiserver/proxy_test.go index 8dd6f68ca9..e00cc484ac 100644 --- a/pkg/apiserver/proxy_test.go +++ b/pkg/apiserver/proxy_test.go @@ -294,7 +294,7 @@ func TestProxy(t *testing.T) { server *httptest.Server proxyTestPattern string }{ - {namespaceServer, "/api/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, + {namespaceServer, "/api/version2/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, {legacyNamespaceServer, "/api/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace}, } @@ -348,7 +348,7 @@ func TestProxyUpgrade(t *testing.T) { server := httptest.NewServer(namespaceHandler) defer server.Close() - ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/api/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/") + ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/api/version2/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/") if err != nil { t.Fatalf("websocket dial err: %s", err) } diff --git a/pkg/apiserver/redirect_test.go b/pkg/apiserver/redirect_test.go index d8ff29c6a0..0963dd98c9 100644 --- a/pkg/apiserver/redirect_test.go +++ b/pkg/apiserver/redirect_test.go @@ -105,7 +105,7 @@ func TestRedirectWithNamespaces(t *testing.T) { for _, item := range table { simpleStorage.errors["resourceLocation"] = item.err simpleStorage.resourceLocation = &url.URL{Host: item.id} - resp, err := client.Get(server.URL + "/api/version/redirect/namespaces/other/foo/" + item.id) + resp, err := client.Get(server.URL + "/api/version2/redirect/namespaces/other/foo/" + item.id) if resp == nil { t.Fatalf("Unexpected nil resp") } diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 25ae6c98a0..7ac81e3afe 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -27,8 +27,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/emicklei/go-restful" @@ -64,9 +62,15 @@ type RequestScope struct { Namer ScopeNamer ContextFunc runtime.Codec + Creater runtime.ObjectCreater + Convertor runtime.ObjectConvertor + Resource string Kind string APIVersion string + + // The version of apiserver resources to use + ServerAPIVersion string } // GetResource returns a function that handles retrieving a single resource from a rest.Storage object. @@ -94,26 +98,8 @@ func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction { } } -func parseSelectorQueryParams(query url.Values, version, apiResource string) (label labels.Selector, field fields.Selector, err error) { - labelString := query.Get(api.LabelSelectorQueryParam(version)) - label, err = labels.Parse(labelString) - if err != nil { - return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'labels' selector parameter (%s) could not be parsed: %v", labelString, err)) - } - - convertToInternalVersionFunc := func(label, value string) (newLabel, newValue string, err error) { - return api.Scheme.ConvertFieldLabel(version, apiResource, label, value) - } - fieldString := query.Get(api.FieldSelectorQueryParam(version)) - field, err = fields.ParseAndTransformSelector(fieldString, convertToInternalVersionFunc) - if err != nil { - return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'fields' selector parameter (%s) could not be parsed: %v", fieldString, err)) - } - return label, field, nil -} - // ListResource returns a function that handles retrieving a list of resources from a rest.Storage object. -func ListResource(r rest.Lister, scope RequestScope) restful.RouteFunction { +func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch bool) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter @@ -125,13 +111,35 @@ func ListResource(r rest.Lister, scope RequestScope) restful.RouteFunction { ctx := scope.ContextFunc(req) ctx = api.WithNamespace(ctx, namespace) - label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), scope.APIVersion, scope.Resource) + out, err := queryToObject(req.Request.URL.Query(), scope, "ListOptions") if err != nil { errorJSON(err, scope.Codec, w) return } + opts := *out.(*api.ListOptions) - result, err := r.List(ctx, label, field) + // transform fields + fn := func(label, value string) (newLabel, newValue string, err error) { + return scope.Convertor.ConvertFieldLabel(scope.APIVersion, scope.Kind, label, value) + } + if opts.FieldSelector, err = opts.FieldSelector.Transform(fn); err != nil { + // TODO: allow bad request to set field causes based on query parameters + err = errors.NewBadRequest(err.Error()) + errorJSON(err, scope.Codec, w) + return + } + + if (opts.Watch || forceWatch) && rw != nil { + watcher, err := rw.Watch(ctx, opts.LabelSelector, opts.FieldSelector, opts.ResourceVersion) + if err != nil { + errorJSON(err, scope.Codec, w) + return + } + serveWatch(watcher, scope, w, req) + return + } + + result, err := r.List(ctx, opts.LabelSelector, opts.FieldSelector) if err != nil { errorJSON(err, scope.Codec, w) return @@ -409,6 +417,27 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, scope RequestScope, } } +// queryToObject converts query parameters into a structured internal object by +// kind. The caller must cast the returned object to the matching internal Kind +// to use it. +// TODO: add appropriate structured error responses +func queryToObject(query url.Values, scope RequestScope, kind string) (runtime.Object, error) { + versioned, err := scope.Creater.New(scope.ServerAPIVersion, kind) + if err != nil { + // programmer error + return nil, err + } + if err := scope.Convertor.Convert(&query, versioned); err != nil { + return nil, errors.NewBadRequest(err.Error()) + } + out, err := scope.Convertor.ConvertToVersion(versioned, "") + if err != nil { + // programmer error + return nil, err + } + return out, nil +} + // resultFunc is a function that returns a rest result and can be run in a goroutine type resultFunc func() (runtime.Object, error) diff --git a/pkg/apiserver/watch.go b/pkg/apiserver/watch.go index c92fdb3002..a95906805a 100644 --- a/pkg/apiserver/watch.go +++ b/pkg/apiserver/watch.go @@ -17,111 +17,38 @@ limitations under the License. package apiserver import ( - "fmt" "net/http" - "path" + "reflect" "regexp" "strings" - "time" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" + "github.com/emicklei/go-restful" "github.com/golang/glog" "golang.org/x/net/websocket" ) -type WatchHandler struct { - storage map[string]rest.Storage - codec runtime.Codec - linker runtime.SelfLinker - info *APIRequestInfoResolver -} - -// setSelfLinkAddName sets the self link, appending the object's name to the canonical path & type. -func (h *WatchHandler) setSelfLinkAddName(obj runtime.Object, req *http.Request) error { - name, err := h.linker.Name(obj) - if err != nil { - return err - } - newURL := *req.URL - newURL.Path = path.Join(req.URL.Path, name) - newURL.RawQuery = "" - newURL.Fragment = "" - return h.linker.SetSelfLink(obj, newURL.String()) -} - var connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)") func isWebsocketRequest(req *http.Request) bool { return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket" } -// ServeHTTP processes watch requests. -func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - var verb string - var apiResource string - var httpCode int - reqStart := time.Now() - defer monitor("watch", &verb, &apiResource, &httpCode, reqStart) - - if req.Method != "GET" { - httpCode = errorJSON(errors.NewBadRequest( - fmt.Sprintf("unsupported method for watch: %s", req.Method)), h.codec, w) - return - } - - requestInfo, err := h.info.GetAPIRequestInfo(req) - if err != nil { - httpCode = errorJSON(errors.NewBadRequest( - fmt.Sprintf("failed to find api request info: %s", err.Error())), h.codec, w) - return - } - verb = requestInfo.Verb - ctx := api.WithNamespace(api.NewContext(), requestInfo.Namespace) - - storage := h.storage[requestInfo.Resource] - if storage == nil { - httpCode = errorJSON(errors.NewNotFound(requestInfo.Resource, "Resource"), h.codec, w) - return - } - apiResource = requestInfo.Resource - watcher, ok := storage.(rest.Watcher) - if !ok { - httpCode = errorJSON(errors.NewMethodNotSupported(requestInfo.Resource, "watch"), h.codec, w) - return - } - - label, field, err := parseSelectorQueryParams(req.URL.Query(), requestInfo.APIVersion, apiResource) - if err != nil { - httpCode = errorJSON(err, h.codec, w) - return - } - - resourceVersion := req.URL.Query().Get("resourceVersion") - watching, err := watcher.Watch(ctx, label, field, resourceVersion) - if err != nil { - httpCode = errorJSON(err, h.codec, w) - return - } - httpCode = http.StatusOK - - // TODO: This is one watch per connection. We want to multiplex, so that - // multiple watches of the same thing don't create two watches downstream. - watchServer := &WatchServer{watching, h.codec, func(obj runtime.Object) { - if err := h.setSelfLinkAddName(obj, req); err != nil { - glog.Errorf("Failed to set self link for object %#v", obj) +// serveWatch handles serving requests to the server +func serveWatch(watcher watch.Interface, scope RequestScope, w http.ResponseWriter, req *restful.Request) { + watchServer := &WatchServer{watcher, scope.Codec, func(obj runtime.Object) { + if err := setSelfLink(obj, req, scope.Namer); err != nil { + glog.V(5).Infof("Failed to set self link for object %v: %v", reflect.TypeOf(obj), err) } }} - if isWebsocketRequest(req) { - websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req) + if isWebsocketRequest(req.Request) { + websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req.Request) } else { - watchServer.ServeHTTP(w, req) + watchServer.ServeHTTP(w, req.Request) } } diff --git a/pkg/apiserver/watch_test.go b/pkg/apiserver/watch_test.go index c4fb3b52d1..d08227f2ad 100644 --- a/pkg/apiserver/watch_test.go +++ b/pkg/apiserver/watch_test.go @@ -51,13 +51,13 @@ var watchTestTable = []struct { func TestWatchWebsocket(t *testing.T) { simpleStorage := &SimpleRESTStorage{} _ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work. - handler := handle(map[string]rest.Storage{"foo": simpleStorage}) + handler := handle(map[string]rest.Storage{"simples": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() dest, _ := url.Parse(server.URL) dest.Scheme = "ws" // Required by websocket, though the server never sees it. - dest.Path = "/api/version/watch/foo" + dest.Path = "/api/version/watch/simples" dest.RawQuery = "" ws, err := websocket.Dial(dest.String(), "", "http://localhost") @@ -103,13 +103,13 @@ func TestWatchWebsocket(t *testing.T) { func TestWatchHTTP(t *testing.T) { simpleStorage := &SimpleRESTStorage{} - handler := handle(map[string]rest.Storage{"foo": simpleStorage}) + handler := handle(map[string]rest.Storage{"simples": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} dest, _ := url.Parse(server.URL) - dest.Path = "/api/version/watch/foo" + dest.Path = "/api/version/watch/simples" dest.RawQuery = "" request, err := http.NewRequest("GET", dest.String(), nil) @@ -163,17 +163,13 @@ func TestWatchHTTP(t *testing.T) { } func TestWatchParamParsing(t *testing.T) { - api.Scheme.AddFieldLabelConversionFunc(testVersion, "foo", - func(label, value string) (string, string, error) { - return label, value, nil - }) simpleStorage := &SimpleRESTStorage{} - handler := handle(map[string]rest.Storage{"foo": simpleStorage}) + handler := handle(map[string]rest.Storage{"simples": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() dest, _ := url.Parse(server.URL) - dest.Path = "/api/" + testVersion + "/watch/foo" + dest.Path = "/api/" + testVersion + "/watch/simples" table := []struct { rawQuery string @@ -189,13 +185,13 @@ func TestWatchParamParsing(t *testing.T) { fieldSelector: "", namespace: api.NamespaceAll, }, { - rawQuery: "namespace=default&resourceVersion=314159&" + api.FieldSelectorQueryParam(testVersion) + "=Host%3D&" + api.LabelSelectorQueryParam(testVersion) + "=name%3Dfoo", + rawQuery: "namespace=default&resourceVersion=314159&fields=Host%3D&labels=name%3Dfoo", resourceVersion: "314159", labelSelector: "name=foo", fieldSelector: "Host=", namespace: api.NamespaceDefault, }, { - rawQuery: "namespace=watchother&" + api.FieldSelectorQueryParam(testVersion) + "=id%3dfoo&resourceVersion=1492", + rawQuery: "namespace=watchother&fields=id%3dfoo&resourceVersion=1492", resourceVersion: "1492", labelSelector: "", fieldSelector: "id=foo", @@ -238,14 +234,14 @@ func TestWatchParamParsing(t *testing.T) { func TestWatchProtocolSelection(t *testing.T) { simpleStorage := &SimpleRESTStorage{} - handler := handle(map[string]rest.Storage{"foo": simpleStorage}) + handler := handle(map[string]rest.Storage{"simples": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() defer server.CloseClientConnections() client := http.Client{} dest, _ := url.Parse(server.URL) - dest.Path = "/api/version/watch/foo" + dest.Path = "/api/version/watch/simples" dest.RawQuery = "" table := []struct { diff --git a/pkg/master/master.go b/pkg/master/master.go index 0bc24afcd3..1979f91537 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -572,9 +572,10 @@ func (m *Master) defaultAPIGroupVersion() *apiserver.APIGroupVersion { Mapper: latest.RESTMapper, - Creater: api.Scheme, - Typer: api.Scheme, - Linker: latest.SelfLinker, + Creater: api.Scheme, + Convertor: api.Scheme, + Typer: api.Scheme, + Linker: latest.SelfLinker, Admit: m.admissionControl, Context: m.requestContextMapper, diff --git a/pkg/runtime/conversion.go b/pkg/runtime/conversion.go index b47f9e6bbe..8b5cb419d2 100644 --- a/pkg/runtime/conversion.go +++ b/pkg/runtime/conversion.go @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Defines conversions between generic types and structs to map query strings +// to struct objects. package runtime import ( @@ -40,6 +42,7 @@ func JSONKeyMapper(key string, sourceTag, destTag reflect.StructTag) (string, st var DefaultStringConversions = []interface{}{ convertStringSliceToString, convertStringSliceToInt, + convertStringSliceToBool, convertStringSliceToInt64, } @@ -64,6 +67,19 @@ func convertStringSliceToInt(input *[]string, out *int, s conversion.Scope) erro return nil } +func convertStringSliceToBool(input *[]string, out *bool, s conversion.Scope) error { + if len(*input) == 0 { + *out = false + } + switch strings.ToLower((*input)[0]) { + case "true", "1": + *out = true + default: + *out = true + } + return nil +} + func convertStringSliceToInt64(input *[]string, out *int64, s conversion.Scope) error { if len(*input) == 0 { *out = 0 diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index 11735027b9..da38b6640b 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -35,7 +35,9 @@ type Codec interface { // ObjectConvertor converts an object to a different version. type ObjectConvertor interface { + Convert(in, out interface{}) error ConvertToVersion(in Object, outVersion string) (out Object, err error) + ConvertFieldLabel(version, kind, label, value string) (string, string, error) } // ObjectTyper contains methods for extracting the APIVersion and Kind diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 7fc233767e..bbbd38889e 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -18,6 +18,7 @@ package runtime import ( "fmt" + "net/url" "reflect" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" @@ -224,6 +225,9 @@ func NewScheme() *Scheme { if err := s.raw.RegisterInputDefaults(&map[string][]string{}, JSONKeyMapper, conversion.AllowDifferentFieldTypeNames|conversion.IgnoreMissingFields); err != nil { panic(err) } + if err := s.raw.RegisterInputDefaults(&url.Values{}, JSONKeyMapper, conversion.AllowDifferentFieldTypeNames|conversion.IgnoreMissingFields); err != nil { + panic(err) + } return s } @@ -294,13 +298,13 @@ func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error { } // AddFieldLabelConversionFunc adds a conversion function to convert field selectors -// of the given api resource from the given version to internal version representation. -func (s *Scheme) AddFieldLabelConversionFunc(version, apiResource string, conversionFunc FieldLabelConversionFunc) error { +// of the given kind from the given version to internal version representation. +func (s *Scheme) AddFieldLabelConversionFunc(version, kind string, conversionFunc FieldLabelConversionFunc) error { if s.fieldLabelConversionFuncs[version] == nil { s.fieldLabelConversionFuncs[version] = map[string]FieldLabelConversionFunc{} } - s.fieldLabelConversionFuncs[version][apiResource] = conversionFunc + s.fieldLabelConversionFuncs[version][kind] = conversionFunc return nil } @@ -326,15 +330,15 @@ func (s *Scheme) Convert(in, out interface{}) error { return s.raw.Convert(in, out) } -// Converts the given field label and value for an apiResource field selector from +// Converts the given field label and value for an kind field selector from // versioned representation to an unversioned one. -func (s *Scheme) ConvertFieldLabel(version, apiResource, label, value string) (string, string, error) { +func (s *Scheme) ConvertFieldLabel(version, kind, label, value string) (string, string, error) { if s.fieldLabelConversionFuncs[version] == nil { return "", "", fmt.Errorf("No conversion function found for version: %s", version) } - conversionFunc, ok := s.fieldLabelConversionFuncs[version][apiResource] + conversionFunc, ok := s.fieldLabelConversionFuncs[version][kind] if !ok { - return "", "", fmt.Errorf("No conversion function found for version %s and api resource %s", version, apiResource) + return "", "", fmt.Errorf("No conversion function found for version %s and kind %s", version, kind) } return conversionFunc(label, value) } diff --git a/pkg/watch/json/types.go b/pkg/watch/json/types.go index 6688e0fbba..66c297a029 100644 --- a/pkg/watch/json/types.go +++ b/pkg/watch/json/types.go @@ -37,6 +37,11 @@ type watchEvent struct { Object runtime.RawExtension `json:"object,omitempty"` } +// NewWatchEvent returns the serialization form of watchEvent for structured schemas +func NewWatchEvent() interface{} { + return &watchEvent{} +} + // Object converts a watch.Event into an appropriately serializable JSON object func Object(codec runtime.Codec, event *watch.Event) (interface{}, error) { obj, ok := event.Object.(runtime.Object)