Convert List query parameters via object conversion

Convert url.Values -> an object, with appropriate versioning. ListOptions
should also expose parameter names to swagger.
This commit is contained in:
Clayton Coleman 2015-03-22 17:43:00 -04:00
parent a2801a5a18
commit 1618c39a46
29 changed files with 417 additions and 98 deletions

View File

@ -43,6 +43,16 @@ kube::util::wait_for_url() {
return 1 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 # 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 # 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. # this info to figure out where the final binaries are placed.

View File

@ -33,12 +33,14 @@ function cleanup()
[[ -n ${PROXY_PID-} ]] && kill ${PROXY_PID} 1>&2 2>/dev/null [[ -n ${PROXY_PID-} ]] && kill ${PROXY_PID} 1>&2 2>/dev/null
kube::etcd::cleanup kube::etcd::cleanup
rm -rf "${KUBE_TEMP}"
kube::log::status "Clean up complete" kube::log::status "Clean up complete"
} }
trap cleanup EXIT SIGINT trap cleanup EXIT SIGINT
kube::util::ensure-temp-dir
kube::etcd::start kube::etcd::start
ETCD_HOST=${ETCD_HOST:-127.0.0.1} 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:" kube::test::describe_object_assert nodes "127.0.0.1" "Name:" "Labels:" "CreationTimestamp:" "Conditions:" "Addresses:" "Capacity:" "Pods:"
########### ###########
# Minions # # Minions #
########### ###########
@ -548,6 +551,7 @@ __EOF__
kube::test::describe_object_assert minions "127.0.0.1" "Name:" "Conditions:" "Addresses:" "Capacity:" "Pods:" kube::test::describe_object_assert minions "127.0.0.1" "Name:" "Conditions:" "Addresses:" "Capacity:" "Pods:"
fi fi
##################### #####################
# Retrieve multiple # # Retrieve multiple #
##################### #####################
@ -555,6 +559,19 @@ __EOF__
kube::log::status "Testing kubectl(${version}:multiget)" 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:' 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}")" ]]
fi
kube::test::clear_all kube::test::clear_all
done done

View File

@ -19,6 +19,8 @@ package api
import ( import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "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/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
) )
@ -28,12 +30,48 @@ import (
var Codec = runtime.CodecFor(Scheme, "") var Codec = runtime.CodecFor(Scheme, "")
func init() { func init() {
Scheme.AddDefaultingFuncs(
func(obj *ListOptions) {
obj.LabelSelector = labels.Everything()
obj.FieldSelector = fields.Everything()
},
)
Scheme.AddConversionFuncs( Scheme.AddConversionFuncs(
func(in *util.Time, out *util.Time, s conversion.Scope) error { func(in *util.Time, out *util.Time, s conversion.Scope) error {
// Cannot deep copy these, because time.Time has unexported fields. // Cannot deep copy these, because time.Time has unexported fields.
*out = *in *out = *in
return nil 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 { func(in *resource.Quantity, out *resource.Quantity, s conversion.Scope) error {
// Cannot deep copy these, because inf.Dec has unexported fields. // Cannot deep copy these, because inf.Dec has unexported fields.
*out = *in.Copy() *out = *in.Copy()

View File

@ -21,6 +21,8 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "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/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
@ -63,6 +65,12 @@ var Semantic = conversion.EqualitiesOrDie(
func(a, b util.Time) bool { func(a, b util.Time) bool {
return a.UTC() == b.UTC() 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( var standardResources = util.NewStringSet(

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: move everything in this file to pkg/api/rest
package meta package meta
import ( import (

View File

@ -38,10 +38,18 @@ func (fakeCodec) DecodeInto([]byte, runtime.Object) error {
type fakeConvertor struct{} type fakeConvertor struct{}
func (fakeConvertor) Convert(in, out interface{}) error {
return nil
}
func (fakeConvertor) ConvertToVersion(in runtime.Object, _ string) (runtime.Object, error) { func (fakeConvertor) ConvertToVersion(in runtime.Object, _ string) (runtime.Object, error) {
return in, nil return in, nil
} }
func (fakeConvertor) ConvertFieldLabel(version, kind, label, value string) (string, string, error) {
return label, value, nil
}
var validCodec = fakeCodec{} var validCodec = fakeCodec{}
var validAccessor = resourceAccessor{} var validAccessor = resourceAccessor{}
var validConvertor = fakeConvertor{} var validConvertor = fakeConvertor{}

View File

@ -52,11 +52,12 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&DeleteOptions{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
&PersistentVolumeClaimList{}, &PersistentVolumeClaimList{},
&DeleteOptions{},
&ListOptions{},
) )
// Legacy names are supported // Legacy names are supported
Scheme.AddKnownTypeWithName("", "Minion", &Node{}) Scheme.AddKnownTypeWithName("", "Minion", &Node{})
@ -90,8 +91,9 @@ func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {}
func (*DeleteOptions) IsAnAPIObject() {}
func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {}
func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {}
func (*DeleteOptions) IsAnAPIObject() {}
func (*ListOptions) IsAnAPIObject() {}

View File

@ -129,7 +129,7 @@ func TestList(t *testing.T) {
} }
var nonRoundTrippableTypes = util.NewStringSet("ContainerManifest", "ContainerManifestList") var nonRoundTrippableTypes = util.NewStringSet("ContainerManifest", "ContainerManifestList")
var nonInternalRoundTrippableTypes = util.NewStringSet("List") var nonInternalRoundTrippableTypes = util.NewStringSet("List", "ListOptions")
func TestRoundTripTypes(t *testing.T) { func TestRoundTripTypes(t *testing.T) {
// api.Scheme.Log(t) // api.Scheme.Log(t)

View File

@ -24,6 +24,8 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "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/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
@ -88,6 +90,11 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)
j.SelfLink = c.RandString() 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) { func(j *api.PodPhase, c fuzz.Continue) {
statuses := []api.PodPhase{api.PodPending, api.PodRunning, api.PodFailed, api.PodUnknown} statuses := []api.PodPhase{api.PodPending, api.PodRunning, api.PodFailed, api.PodUnknown}
*j = statuses[c.Rand.Intn(len(statuses))] *j = statuses[c.Rand.Intn(len(statuses))]

View File

@ -18,6 +18,8 @@ package api
import ( import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "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/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
@ -1168,6 +1170,16 @@ type DeleteOptions struct {
GracePeriodSeconds *int64 `json:"gracePeriodSeconds"` GracePeriodSeconds *int64 `json:"gracePeriodSeconds"`
} }
// ListOptions is the query options to a standard REST list call
type ListOptions struct {
TypeMeta `json:",inline"`
// A selector based on labels
LabelSelector labels.Selector
// A selector based on fields
FieldSelector fields.Selector
}
// Status is a return value for calls that don't return other objects. // 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 // TODO: this could go in apiserver, but I'm including it here so clients needn't
// import both. // import both.

View File

@ -40,18 +40,20 @@ func PreV1Beta3(version string) bool {
return version == "v1beta1" || version == "v1beta2" return version == "v1beta1" || version == "v1beta2"
} }
// TODO: remove me when watch is refactored
func LabelSelectorQueryParam(version string) string { func LabelSelectorQueryParam(version string) string {
if PreV1Beta3(version) { if PreV1Beta3(version) {
return "labels" return "labels"
} }
return "label-selector" return "labelSelector"
} }
// TODO: remove me when watch is refactored
func FieldSelectorQueryParam(version string) string { func FieldSelectorQueryParam(version string) string {
if PreV1Beta3(version) { if PreV1Beta3(version) {
return "fields" return "fields"
} }
return "field-selector" return "fieldSelector"
} }
// String returns available api versions as a human-friendly version string. // String returns available api versions as a human-friendly version string.

View File

@ -1494,7 +1494,7 @@ func init() {
} }
// Add field conversion funcs. // Add field conversion funcs.
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "pods", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Pod",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "name": case "name":
@ -1514,7 +1514,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "replicationControllers", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "ReplicationController",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "name": case "name":
@ -1529,7 +1529,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "events", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Event",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "involvedObject.kind", case "involvedObject.kind",
@ -1551,7 +1551,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Namespace",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "status.phase": case "status.phase":

View File

@ -59,11 +59,12 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&DeleteOptions{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
&PersistentVolumeClaimList{}, &PersistentVolumeClaimList{},
&DeleteOptions{},
&ListOptions{},
) )
// Future names are supported // Future names are supported
api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{})
@ -97,8 +98,9 @@ func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {}
func (*DeleteOptions) IsAnAPIObject() {}
func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {}
func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {}
func (*DeleteOptions) IsAnAPIObject() {}
func (*ListOptions) IsAnAPIObject() {}

View File

@ -985,6 +985,16 @@ 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"` 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"`
}
// Status is a return value for calls that don't return other objects. // 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 // TODO: this could go in apiserver, but I'm including it here so clients needn't
// import both. // import both.

View File

@ -1420,7 +1420,7 @@ func init() {
} }
// Add field conversion funcs. // Add field conversion funcs.
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "pods", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "Pod",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "name": case "name":
@ -1440,7 +1440,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "replicationControllers", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "ReplicationController",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "name": case "name":
@ -1455,7 +1455,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "events", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "Event",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "involvedObject.kind", case "involvedObject.kind",
@ -1477,7 +1477,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Namespace",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "status.phase": case "status.phase":

View File

@ -59,11 +59,12 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&DeleteOptions{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
&PersistentVolumeClaimList{}, &PersistentVolumeClaimList{},
&DeleteOptions{},
&ListOptions{},
) )
// Future names are supported // Future names are supported
api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{})
@ -102,3 +103,4 @@ func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {}
func (*DeleteOptions) IsAnAPIObject() {} func (*DeleteOptions) IsAnAPIObject() {}
func (*ListOptions) IsAnAPIObject() {}

View File

@ -999,6 +999,16 @@ 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"` 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"`
}
// Status is a return value for calls that don't return other objects. // 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 // TODO: this could go in apiserver, but I'm including it here so clients needn't
// import both. // import both.

View File

@ -24,7 +24,7 @@ import (
func init() { func init() {
// Add field conversion funcs. // Add field conversion funcs.
err := newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "pods", err := newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "Pod",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "name", case "name",
@ -39,7 +39,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "replicationControllers", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "ReplicationController",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "name": case "name":
@ -54,7 +54,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "events", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "Event",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "involvedObject.kind", case "involvedObject.kind",
@ -75,7 +75,7 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "Namespace",
func(label, value string) (string, string, error) { func(label, value string) (string, string, error) {
switch label { switch label {
case "status.phase": case "status.phase":

View File

@ -53,11 +53,12 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&DeleteOptions{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
&PersistentVolumeClaimList{}, &PersistentVolumeClaimList{},
&DeleteOptions{},
&ListOptions{},
) )
// Legacy names are supported // Legacy names are supported
api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{})
@ -96,3 +97,4 @@ func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {}
func (*DeleteOptions) IsAnAPIObject() {} func (*DeleteOptions) IsAnAPIObject() {}
func (*ListOptions) IsAnAPIObject() {}

View File

@ -1158,6 +1158,16 @@ 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"` 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"`
}
// Status is a return value for calls that don't return other objects. // Status is a return value for calls that don't return other objects.
type Status struct { type Status struct {
TypeMeta `json:",inline"` TypeMeta `json:",inline"`

View File

@ -29,6 +29,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
@ -60,10 +61,12 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) {
// Initialize the custom handlers. // Initialize the custom handlers.
watchHandler := (&WatchHandler{ watchHandler := (&WatchHandler{
storage: a.group.Storage, storage: a.group.Storage,
codec: a.group.Codec, mapper: a.group.Mapper,
linker: a.group.Linker, convertor: a.group.Convertor,
info: a.info, codec: a.group.Codec,
linker: a.group.Linker,
info: a.info,
}) })
redirectHandler := (&RedirectHandler{a.group.Storage, a.group.Codec, a.group.Context, 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}) proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info})
@ -99,6 +102,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
admit := a.group.Admit admit := a.group.Admit
context := a.group.Context context := a.group.Context
serverVersion := a.group.ServerVersion
if len(serverVersion) == 0 {
serverVersion = a.group.Version
}
var resource, subresource string var resource, subresource string
switch parts := strings.Split(path, "/"); len(parts) { switch parts := strings.Split(path, "/"); len(parts) {
case 2: case 2:
@ -152,10 +160,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
storageMeta = defaultStorageMetadata{} storageMeta = defaultStorageMetadata{}
} }
versionedListOptions, err := a.group.Creater.New(serverVersion, "ListOptions")
if err != nil {
return err
}
var versionedDeleterObject runtime.Object var versionedDeleterObject runtime.Object
switch { switch {
case isGracefulDeleter: case isGracefulDeleter:
object, err := a.group.Creater.New(a.group.Version, "DeleteOptions") object, err := a.group.Creater.New(serverVersion, "DeleteOptions")
if err != nil { if err != nil {
return err return err
} }
@ -288,11 +301,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
// test/integration/auth_test.go is currently the most comprehensive status code test // test/integration/auth_test.go is currently the most comprehensive status code test
reqScope := RequestScope{ reqScope := RequestScope{
ContextFunc: ctxFn, ContextFunc: ctxFn,
Codec: mapping.Codec, Creater: a.group.Creater,
APIVersion: a.group.Version, Convertor: a.group.Convertor,
Resource: resource, Codec: mapping.Codec,
Kind: kind, APIVersion: a.group.Version,
ServerAPIVersion: serverVersion,
Resource: resource,
Kind: kind,
} }
for _, action := range actions { for _, action := range actions {
reqScope.Namer = action.Namer reqScope.Namer = action.Namer
@ -314,6 +330,9 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Operation("list" + kind). Operation("list" + kind).
Produces("application/json"). Produces("application/json").
Writes(versionedList) Writes(versionedList)
if err := addObjectParams(ws, route, versionedListOptions); err != nil {
return err
}
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
case "PUT": // Update a resource. case "PUT": // Update a resource.
@ -651,6 +670,39 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
} }
} }
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. // defaultStorageMetadata provides default answers to rest.StorageMetadata.
type defaultStorageMetadata struct{} type defaultStorageMetadata struct{}

View File

@ -102,12 +102,19 @@ type APIGroupVersion struct {
Root string Root string
Version 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 Mapper meta.RESTMapper
Codec runtime.Codec Codec runtime.Codec
Typer runtime.ObjectTyper Typer runtime.ObjectTyper
Creater runtime.ObjectCreater Creater runtime.ObjectCreater
Linker runtime.SelfLinker Convertor runtime.ObjectConvertor
Linker runtime.SelfLinker
Admit admission.Interface Admit admission.Interface
Context api.RequestContextMapper Context api.RequestContextMapper

View File

@ -37,6 +37,7 @@ import (
apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@ -96,11 +97,10 @@ func init() {
// api.Status is returned in errors // api.Status is returned in errors
// "internal" version // "internal" version
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, &api.Status{}, &api.ListOptions{})
&api.Status{})
// "version" version // "version" version
// TODO: Use versioned api objects? // TODO: Use versioned api objects?
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &api.DeleteOptions{}, &api.Status{}) api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &v1beta1.DeleteOptions{}, &v1beta1.Status{}, &v1beta1.ListOptions{})
nsMapper := newMapper() nsMapper := newMapper()
legacyNsMapper := newMapper() legacyNsMapper := newMapper()
@ -156,10 +156,11 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
Root: "/api", Root: "/api",
Version: testVersion, Version: testVersion,
Creater: api.Scheme, Creater: api.Scheme,
Typer: api.Scheme, Convertor: api.Scheme,
Codec: codec, Typer: api.Scheme,
Linker: selfLinker, Codec: codec,
Linker: selfLinker,
Admit: admissionControl, Admit: admissionControl,
Context: requestContextMapper, Context: requestContextMapper,
@ -244,6 +245,8 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, f
result := &SimpleList{ result := &SimpleList{
Items: storage.list, Items: storage.list,
} }
storage.requestedLabelSelector = label
storage.requestedFieldSelector = field
return result, storage.errors["list"] return result, storage.errors["list"]
} }
@ -522,15 +525,60 @@ func TestList(t *testing.T) {
namespace string namespace string
selfLink string selfLink string
legacy bool 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 // 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}, url: "/api/version/simple?namespace=",
{"/api/version/namespaces/other/simple", "other", "/api/version/namespaces/other/simple", false}, namespace: "",
selfLink: "/api/version/simple?namespace=",
legacy: true,
},
// list items in a namespace, v1beta3+
{
url: "/api/version/namespaces/default/simple",
namespace: "default",
selfLink: "/api/version/namespaces/default/simple",
},
{
url: "/api/version/namespaces/other/simple",
namespace: "other",
selfLink: "/api/version/namespaces/other/simple",
},
{
url: "/api/version/namespaces/other/simple?labels=a%3Db&fields=c%3Dd",
namespace: "other",
selfLink: "/api/version/namespaces/other/simple",
label: "a=b",
field: "c=d",
},
// list items across all namespaces // list items across all namespaces
{"/api/version/simple", "", "/api/version/simple", false}, {
url: "/api/version/simple",
namespace: "",
selfLink: "/api/version/simple",
},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
storage := map[string]rest.Storage{} storage := map[string]rest.Storage{}
@ -567,6 +615,12 @@ func TestList(t *testing.T) {
} else if simpleStorage.actualNamespace != testCase.namespace { } else if simpleStorage.actualNamespace != testCase.namespace {
t.Errorf("%d: unexpected resource namespace: %s", i, simpleStorage.actualNamespace) 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)
}
} }
} }

View File

@ -19,7 +19,6 @@ package apiserver
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
gpath "path" gpath "path"
"time" "time"
@ -27,8 +26,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "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/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
@ -64,9 +61,15 @@ type RequestScope struct {
Namer ScopeNamer Namer ScopeNamer
ContextFunc ContextFunc
runtime.Codec runtime.Codec
Creater runtime.ObjectCreater
Convertor runtime.ObjectConvertor
Resource string Resource string
Kind string Kind string
APIVersion 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. // GetResource returns a function that handles retrieving a single resource from a rest.Storage object.
@ -94,24 +97,6 @@ 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. // 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, scope RequestScope) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) { return func(req *restful.Request, res *restful.Response) {
@ -125,13 +110,38 @@ func ListResource(r rest.Lister, scope RequestScope) restful.RouteFunction {
ctx := scope.ContextFunc(req) ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace) ctx = api.WithNamespace(ctx, namespace)
label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), scope.APIVersion, scope.Resource) // TODO: extract me into a method
query := req.Request.URL.Query()
versioned, err := scope.Creater.New(scope.ServerAPIVersion, "ListOptions")
if err != nil { if err != nil {
// programmer error
errorJSON(err, scope.Codec, w)
return
}
if err := scope.Convertor.Convert(&query, versioned); err != nil {
// bad request
errorJSON(err, scope.Codec, w)
return
}
out, err := scope.Convertor.ConvertToVersion(versioned, "")
if err != nil {
// programmer error
errorJSON(err, scope.Codec, w)
return
}
opts := *out.(*api.ListOptions)
// 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 {
// invalid field
errorJSON(err, scope.Codec, w) errorJSON(err, scope.Codec, w)
return return
} }
result, err := r.List(ctx, label, field) result, err := r.List(ctx, opts.LabelSelector, opts.FieldSelector)
if err != nil { if err != nil {
errorJSON(err, scope.Codec, w) errorJSON(err, scope.Codec, w)
return return

View File

@ -19,6 +19,7 @@ package apiserver
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"path" "path"
"regexp" "regexp"
"strings" "strings"
@ -26,8 +27,11 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "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/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
@ -36,11 +40,14 @@ import (
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
) )
// TODO: convert me to resthandler custom verb
type WatchHandler struct { type WatchHandler struct {
storage map[string]rest.Storage storage map[string]rest.Storage
codec runtime.Codec mapper meta.RESTMapper
linker runtime.SelfLinker convertor runtime.ObjectConvertor
info *APIRequestInfoResolver codec runtime.Codec
linker runtime.SelfLinker
info *APIRequestInfoResolver
} }
// setSelfLinkAddName sets the self link, appending the object's name to the canonical path & type. // setSelfLinkAddName sets the self link, appending the object's name to the canonical path & type.
@ -96,8 +103,24 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
httpCode = errorJSON(errors.NewMethodNotSupported(requestInfo.Resource, "watch"), h.codec, w) httpCode = errorJSON(errors.NewMethodNotSupported(requestInfo.Resource, "watch"), h.codec, w)
return return
} }
kind := requestInfo.Kind
if len(kind) == 0 {
if _, kind, err = h.mapper.VersionAndKindForResource(apiResource); err != nil {
glog.Errorf("No kind found for %s: %v", apiResource, err)
}
}
label, field, err := parseSelectorQueryParams(req.URL.Query(), requestInfo.APIVersion, apiResource) scope := RequestScope{
Convertor: h.convertor,
Kind: kind,
Resource: apiResource,
APIVersion: requestInfo.APIVersion,
// TODO: this must be parameterized per version, and is incorrect for implementors
// outside of Kubernetes. Fix by refactoring watch under resthandler as a custome
// resource.
ServerAPIVersion: requestInfo.APIVersion,
}
label, field, err := parseSelectorQueryParams(req.URL.Query(), scope)
if err != nil { if err != nil {
httpCode = errorJSON(err, h.codec, w) httpCode = errorJSON(err, h.codec, w)
return return
@ -125,6 +148,26 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
} }
// TODO: remove when watcher is refactored to fit under api_installer
func parseSelectorQueryParams(query url.Values, scope RequestScope) (label labels.Selector, field fields.Selector, err error) {
labelString := query.Get(api.LabelSelectorQueryParam(scope.ServerAPIVersion))
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))
}
fn := func(label, value string) (newLabel, newValue string, err error) {
return scope.Convertor.ConvertFieldLabel(scope.APIVersion, scope.Kind, label, value)
}
fieldString := query.Get(api.FieldSelectorQueryParam(scope.ServerAPIVersion))
field, err = fields.ParseAndTransformSelector(fieldString, fn)
if err != nil {
return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'fields' selector parameter (%s) could not be parsed: %v", fieldString, err))
}
glog.Infof("Found %#v %#v from %v in scope %#v", label, field, query, scope)
return label, field, nil
}
// WatchServer serves a watch.Interface over a websocket or vanilla HTTP. // WatchServer serves a watch.Interface over a websocket or vanilla HTTP.
type WatchServer struct { type WatchServer struct {
watching watch.Interface watching watch.Interface

View File

@ -25,6 +25,7 @@ import (
"testing" "testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
@ -48,16 +49,24 @@ var watchTestTable = []struct {
{watch.Deleted, &Simple{ObjectMeta: api.ObjectMeta{Name: "bar"}}}, {watch.Deleted, &Simple{ObjectMeta: api.ObjectMeta{Name: "bar"}}},
} }
func init() {
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
})
}
func TestWatchWebsocket(t *testing.T) { func TestWatchWebsocket(t *testing.T) {
simpleStorage := &SimpleRESTStorage{} simpleStorage := &SimpleRESTStorage{}
_ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work. _ = 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) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
dest, _ := url.Parse(server.URL) dest, _ := url.Parse(server.URL)
dest.Scheme = "ws" // Required by websocket, though the server never sees it. 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 = "" dest.RawQuery = ""
ws, err := websocket.Dial(dest.String(), "", "http://localhost") ws, err := websocket.Dial(dest.String(), "", "http://localhost")
@ -103,13 +112,13 @@ func TestWatchWebsocket(t *testing.T) {
func TestWatchHTTP(t *testing.T) { func TestWatchHTTP(t *testing.T) {
simpleStorage := &SimpleRESTStorage{} simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{"foo": simpleStorage}) handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
client := http.Client{} client := http.Client{}
dest, _ := url.Parse(server.URL) dest, _ := url.Parse(server.URL)
dest.Path = "/api/version/watch/foo" dest.Path = "/api/version/watch/simples"
dest.RawQuery = "" dest.RawQuery = ""
request, err := http.NewRequest("GET", dest.String(), nil) request, err := http.NewRequest("GET", dest.String(), nil)
@ -163,17 +172,13 @@ func TestWatchHTTP(t *testing.T) {
} }
func TestWatchParamParsing(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{} simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{"foo": simpleStorage}) handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
dest, _ := url.Parse(server.URL) dest, _ := url.Parse(server.URL)
dest.Path = "/api/" + testVersion + "/watch/foo" dest.Path = "/api/" + testVersion + "/watch/simples"
table := []struct { table := []struct {
rawQuery string rawQuery string
@ -238,14 +243,14 @@ func TestWatchParamParsing(t *testing.T) {
func TestWatchProtocolSelection(t *testing.T) { func TestWatchProtocolSelection(t *testing.T) {
simpleStorage := &SimpleRESTStorage{} simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{"foo": simpleStorage}) handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
defer server.CloseClientConnections() defer server.CloseClientConnections()
client := http.Client{} client := http.Client{}
dest, _ := url.Parse(server.URL) dest, _ := url.Parse(server.URL)
dest.Path = "/api/version/watch/foo" dest.Path = "/api/version/watch/simples"
dest.RawQuery = "" dest.RawQuery = ""
table := []struct { table := []struct {

View File

@ -572,9 +572,10 @@ func (m *Master) defaultAPIGroupVersion() *apiserver.APIGroupVersion {
Mapper: latest.RESTMapper, Mapper: latest.RESTMapper,
Creater: api.Scheme, Creater: api.Scheme,
Typer: api.Scheme, Convertor: api.Scheme,
Linker: latest.SelfLinker, Typer: api.Scheme,
Linker: latest.SelfLinker,
Admit: m.admissionControl, Admit: m.admissionControl,
Context: m.requestContextMapper, Context: m.requestContextMapper,

View File

@ -35,7 +35,9 @@ type Codec interface {
// ObjectConvertor converts an object to a different version. // ObjectConvertor converts an object to a different version.
type ObjectConvertor interface { type ObjectConvertor interface {
Convert(in, out interface{}) error
ConvertToVersion(in Object, outVersion string) (out Object, err 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 // ObjectTyper contains methods for extracting the APIVersion and Kind

View File

@ -18,6 +18,7 @@ package runtime
import ( import (
"fmt" "fmt"
"net/url"
"reflect" "reflect"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "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 { if err := s.raw.RegisterInputDefaults(&map[string][]string{}, JSONKeyMapper, conversion.AllowDifferentFieldTypeNames|conversion.IgnoreMissingFields); err != nil {
panic(err) panic(err)
} }
if err := s.raw.RegisterInputDefaults(&url.Values{}, JSONKeyMapper, conversion.AllowDifferentFieldTypeNames|conversion.IgnoreMissingFields); err != nil {
panic(err)
}
return s return s
} }
@ -294,13 +298,13 @@ func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error {
} }
// AddFieldLabelConversionFunc adds a conversion function to convert field selectors // AddFieldLabelConversionFunc adds a conversion function to convert field selectors
// of the given api resource from the given version to internal version representation. // of the given kind from the given version to internal version representation.
func (s *Scheme) AddFieldLabelConversionFunc(version, apiResource string, conversionFunc FieldLabelConversionFunc) error { func (s *Scheme) AddFieldLabelConversionFunc(version, kind string, conversionFunc FieldLabelConversionFunc) error {
if s.fieldLabelConversionFuncs[version] == nil { if s.fieldLabelConversionFuncs[version] == nil {
s.fieldLabelConversionFuncs[version] = map[string]FieldLabelConversionFunc{} s.fieldLabelConversionFuncs[version] = map[string]FieldLabelConversionFunc{}
} }
s.fieldLabelConversionFuncs[version][apiResource] = conversionFunc s.fieldLabelConversionFuncs[version][kind] = conversionFunc
return nil return nil
} }
@ -326,15 +330,15 @@ func (s *Scheme) Convert(in, out interface{}) error {
return s.raw.Convert(in, out) 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. // 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 { if s.fieldLabelConversionFuncs[version] == nil {
return "", "", fmt.Errorf("No conversion function found for version: %s", version) 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 { 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) return conversionFunc(label, value)
} }