Merge pull request #5763 from smarterclayton/get_input_parameters_versioned

Expose versioned query parameters and make watch an operation on List
This commit is contained in:
Brian Grant
2015-03-27 14:35:23 -07:00
33 changed files with 538 additions and 226 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,20 @@ __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}")" ]]
[[ "$(grep "watch for changes to the described resources" "${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

@@ -23,6 +23,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"
@@ -87,6 +89,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"
@@ -1201,6 +1203,21 @@ type DeleteOptions struct {
GracePeriodSeconds *int64 `json:"gracePeriodSeconds"` 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. // 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

@@ -1493,7 +1493,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":
@@ -1513,7 +1513,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":
@@ -1528,7 +1528,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",
@@ -1550,7 +1550,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

@@ -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"` 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. // 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

@@ -1419,7 +1419,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":
@@ -1439,7 +1439,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":
@@ -1454,7 +1454,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",
@@ -1476,7 +1476,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

@@ -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"` 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. // 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

@@ -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"` 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. // 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,7 +29,9 @@ 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"
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
) )
@@ -58,13 +60,6 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) {
// Create the WebService. // Create the WebService.
ws = a.newWebService() 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}) 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})
@@ -77,7 +72,7 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) {
} }
sort.Strings(paths) sort.Strings(paths)
for _, path := range 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) errors = append(errors, err)
} }
} }
@@ -95,10 +90,15 @@ func (a *APIInstaller) newWebService() *restful.WebService {
return ws 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 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:
@@ -121,17 +121,6 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
} }
versionedObject := indirectArbitraryPointer(versionedPtr) 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) mapping, err := a.group.Mapper.RESTMapping(kind, a.group.Version)
if err != nil { if err != nil {
return err return err
@@ -145,17 +134,33 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter) gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)
updater, isUpdater := storage.(rest.Updater) updater, isUpdater := storage.(rest.Updater)
patcher, isPatcher := storage.(rest.Patcher) patcher, isPatcher := storage.(rest.Patcher)
_, isWatcher := storage.(rest.Watcher) watcher, isWatcher := storage.(rest.Watcher)
_, isRedirector := storage.(rest.Redirector) _, isRedirector := storage.(rest.Redirector)
storageMeta, isMetadata := storage.(rest.StorageMetadata) storageMeta, isMetadata := storage.(rest.StorageMetadata)
if !isMetadata { if !isMetadata {
storageMeta = defaultStorageMetadata{} 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 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 +293,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
@@ -308,12 +316,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
case "LIST": // List all resources of a kind. 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). Filter(m).
Doc("list objects of kind " + kind). Doc("list objects of kind " + kind).
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.
@@ -356,22 +367,30 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
} }
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
// TODO: deprecated
case "WATCH": // Watch a resource. 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). Filter(m).
Doc("watch a particular " + kind). Doc("watch changes to an object of kind " + kind).
Operation("watch" + kind). Operation("watch" + kind).
Produces("application/json"). Produces("application/json").
Writes(versionedObject) Writes(watchjson.NewWatchEvent())
if err := addObjectParams(ws, route, versionedListOptions); err != nil {
return err
}
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
// TODO: deprecated
case "WATCHLIST": // Watch all resources of a kind. 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). Filter(m).
Doc("watch a list of " + kind). Doc("watch individual changes to a list of " + kind).
Operation("watch" + kind + "list"). Operation("watch" + kind + "list").
Produces("application/json"). Produces("application/json").
Writes(versionedList) Writes(watchjson.NewWatchEvent())
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 "REDIRECT": // Get the redirect URL for a resource. 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. // 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
@@ -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) { 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 { if stream, ok := object.(rest.ResourceStreamer); ok {
out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept")) out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept"))

View File

@@ -37,6 +37,8 @@ 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/api/v1beta3"
"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"
@@ -53,11 +55,21 @@ func convert(obj runtime.Object) (runtime.Object, error) {
return obj, nil 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" const testVersion = "version"
var versions = []string{testVersion} // The equivalent of the Kubernetes v1beta3 API.
var codec = runtime.CodecFor(api.Scheme, testVersion) 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 accessor = meta.NewAccessor()
var versioner runtime.ResourceVersioner = accessor var versioner runtime.ResourceVersioner = accessor
var selfLinker runtime.SelfLinker = accessor var selfLinker runtime.SelfLinker = accessor
@@ -68,6 +80,12 @@ var requestContextMapper api.RequestContextMapper
func interfacesFor(version string) (*meta.VersionInterfaces, error) { func interfacesFor(version string) (*meta.VersionInterfaces, error) {
switch version { switch version {
case testVersion: case testVersion:
return &meta.VersionInterfaces{
Codec: legacyCodec,
ObjectConvertor: api.Scheme,
MetadataAccessor: accessor,
}, nil
case testVersion2:
return &meta.VersionInterfaces{ return &meta.VersionInterfaces{
Codec: codec, Codec: codec,
ObjectConvertor: api.Scheme, ObjectConvertor: api.Scheme,
@@ -96,11 +114,13 @@ 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.Status{})
// "version2" version
// TODO: Use versioned api objects?
api.Scheme.AddKnownTypes(testVersion2, &Simple{}, &SimpleList{}, &v1beta3.Status{})
nsMapper := newMapper() nsMapper := newMapper()
legacyNsMapper := newMapper() legacyNsMapper := newMapper()
@@ -118,6 +138,18 @@ func init() {
namespaceMapper = nsMapper namespaceMapper = nsMapper
admissionControl = admit.NewAlwaysAdmit() admissionControl = admit.NewAlwaysAdmit()
requestContextMapper = api.NewRequestContextMapper() 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. // defaultAPIServer exposes nested objects for testability.
@@ -129,45 +161,61 @@ type defaultAPIServer struct {
// uses the default settings // uses the default settings
func handle(storage map[string]rest.Storage) http.Handler { 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 // tests with a deny admission controller
func handleDeny(storage map[string]rest.Storage) http.Handler { 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 // tests using the new namespace scope mechanism
func handleNamespaced(storage map[string]rest.Storage) http.Handler { 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 // tests using a custom self linker
func handleLinker(storage map[string]rest.Storage, selfLinker runtime.SelfLinker) http.Handler { 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{ group := &APIGroupVersion{
Storage: storage, Storage: storage,
Mapper: mapper, Root: "/api",
Root: "/api", Creater: api.Scheme,
Version: testVersion, Convertor: api.Scheme,
Typer: api.Scheme,
Creater: api.Scheme, Linker: selfLinker,
Typer: api.Scheme,
Codec: codec,
Linker: selfLinker,
Admit: admissionControl, Admit: admissionControl,
Context: requestContextMapper, 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 := restful.NewContainer()
container.Router(restful.CurlyRouter{}) container.Router(restful.CurlyRouter{})
mux := container.ServeMux 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) ws := new(restful.WebService)
InstallSupport(mux, ws) InstallSupport(mux, ws)
container.Add(ws) container.Add(ws)
@@ -244,6 +292,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 +572,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/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 // 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 { for i, testCase := range testCases {
storage := map[string]rest.Storage{} storage := map[string]rest.Storage{}
@@ -545,7 +640,7 @@ func TestList(t *testing.T) {
if testCase.legacy { if testCase.legacy {
handler = handleLinker(storage, selfLinker) handler = handleLinker(storage, selfLinker)
} else { } else {
handler = handleInternal(storage, admissionControl, namespaceMapper, selfLinker) handler = handleInternal(false, storage, admissionControl, selfLinker)
} }
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
@@ -557,6 +652,9 @@ func TestList(t *testing.T) {
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
t.Errorf("%d: unexpected status: %d, Expected: %d, %#v", i, resp.StatusCode, http.StatusOK, resp) 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 // TODO: future, restore get links
if !selfLinker.called { if !selfLinker.called {
@@ -567,6 +665,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)
}
} }
} }
@@ -821,16 +925,16 @@ func TestGetNamespaceSelfLink(t *testing.T) {
} }
selfLinker := &setTestSelfLinker{ selfLinker := &setTestSelfLinker{
t: t, t: t,
expectedSet: "/api/version/namespaces/foo/simple/id", expectedSet: "/api/version2/namespaces/foo/simple/id",
name: "id", name: "id",
namespace: "foo", namespace: "foo",
} }
storage["simple"] = &simpleStorage storage["simple"] = &simpleStorage
handler := handleInternal(storage, admissionControl, namespaceMapper, selfLinker) handler := handleInternal(false, storage, admissionControl, selfLinker)
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() 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 { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@@ -905,7 +1009,7 @@ func TestDeleteWithOptions(t *testing.T) {
item := &api.DeleteOptions{ item := &api.DeleteOptions{
GracePeriodSeconds: &grace, GracePeriodSeconds: &grace,
} }
body, err := codec.Encode(item) body, err := versionServerCodec.Encode(item)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@@ -966,7 +1070,7 @@ func TestLegacyDeleteIgnoresOptions(t *testing.T) {
defer server.Close() defer server.Close()
item := api.NewDeleteOptions(300) item := api.NewDeleteOptions(300)
body, err := codec.Encode(item) body, err := versionServerCodec.Encode(item)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@@ -1575,7 +1679,7 @@ func TestCreateInvokesAdmissionControl(t *testing.T) {
namespace: "other", namespace: "other",
expectedSet: "/api/version/foo/bar?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) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
client := http.Client{} client := http.Client{}

View File

@@ -294,7 +294,7 @@ func TestProxy(t *testing.T) {
server *httptest.Server server *httptest.Server
proxyTestPattern string 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}, {legacyNamespaceServer, "/api/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace},
} }
@@ -348,7 +348,7 @@ func TestProxyUpgrade(t *testing.T) {
server := httptest.NewServer(namespaceHandler) server := httptest.NewServer(namespaceHandler)
defer server.Close() 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 { if err != nil {
t.Fatalf("websocket dial err: %s", err) t.Fatalf("websocket dial err: %s", err)
} }

View File

@@ -105,7 +105,7 @@ func TestRedirectWithNamespaces(t *testing.T) {
for _, item := range table { for _, item := range table {
simpleStorage.errors["resourceLocation"] = item.err simpleStorage.errors["resourceLocation"] = item.err
simpleStorage.resourceLocation = &url.URL{Host: item.id} 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 { if resp == nil {
t.Fatalf("Unexpected nil resp") t.Fatalf("Unexpected nil resp")
} }

View File

@@ -27,8 +27,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 +62,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,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. // 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) { return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter w := res.ResponseWriter
@@ -125,13 +111,35 @@ 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) out, err := queryToObject(req.Request.URL.Query(), scope, "ListOptions")
if err != nil { if err != nil {
errorJSON(err, scope.Codec, w) errorJSON(err, scope.Codec, w)
return 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 { if err != nil {
errorJSON(err, scope.Codec, w) errorJSON(err, scope.Codec, w)
return 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 // resultFunc is a function that returns a rest result and can be run in a goroutine
type resultFunc func() (runtime.Object, error) type resultFunc func() (runtime.Object, error)

View File

@@ -17,111 +17,38 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"fmt"
"net/http" "net/http"
"path" "reflect"
"regexp" "regexp"
"strings" "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/httplog"
"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"
"github.com/emicklei/go-restful"
"github.com/golang/glog" "github.com/golang/glog"
"golang.org/x/net/websocket" "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*,)") var connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)")
func isWebsocketRequest(req *http.Request) bool { func isWebsocketRequest(req *http.Request) bool {
return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket" return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket"
} }
// ServeHTTP processes watch requests. // serveWatch handles serving requests to the server
func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func serveWatch(watcher watch.Interface, scope RequestScope, w http.ResponseWriter, req *restful.Request) {
var verb string watchServer := &WatchServer{watcher, scope.Codec, func(obj runtime.Object) {
var apiResource string if err := setSelfLink(obj, req, scope.Namer); err != nil {
var httpCode int glog.V(5).Infof("Failed to set self link for object %v: %v", reflect.TypeOf(obj), err)
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)
} }
}} }}
if isWebsocketRequest(req) { if isWebsocketRequest(req.Request) {
websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req) websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req.Request)
} else { } else {
watchServer.ServeHTTP(w, req) watchServer.ServeHTTP(w, req.Request)
} }
} }

View File

@@ -51,13 +51,13 @@ var watchTestTable = []struct {
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 +103,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 +163,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
@@ -189,13 +185,13 @@ func TestWatchParamParsing(t *testing.T) {
fieldSelector: "", fieldSelector: "",
namespace: api.NamespaceAll, 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", resourceVersion: "314159",
labelSelector: "name=foo", labelSelector: "name=foo",
fieldSelector: "Host=", fieldSelector: "Host=",
namespace: api.NamespaceDefault, namespace: api.NamespaceDefault,
}, { }, {
rawQuery: "namespace=watchother&" + api.FieldSelectorQueryParam(testVersion) + "=id%3dfoo&resourceVersion=1492", rawQuery: "namespace=watchother&fields=id%3dfoo&resourceVersion=1492",
resourceVersion: "1492", resourceVersion: "1492",
labelSelector: "", labelSelector: "",
fieldSelector: "id=foo", fieldSelector: "id=foo",
@@ -238,14 +234,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

@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// Defines conversions between generic types and structs to map query strings
// to struct objects.
package runtime package runtime
import ( import (
@@ -40,6 +42,7 @@ func JSONKeyMapper(key string, sourceTag, destTag reflect.StructTag) (string, st
var DefaultStringConversions = []interface{}{ var DefaultStringConversions = []interface{}{
convertStringSliceToString, convertStringSliceToString,
convertStringSliceToInt, convertStringSliceToInt,
convertStringSliceToBool,
convertStringSliceToInt64, convertStringSliceToInt64,
} }
@@ -64,6 +67,19 @@ func convertStringSliceToInt(input *[]string, out *int, s conversion.Scope) erro
return nil 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 { func convertStringSliceToInt64(input *[]string, out *int64, s conversion.Scope) error {
if len(*input) == 0 { if len(*input) == 0 {
*out = 0 *out = 0

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)
} }

View File

@@ -37,6 +37,11 @@ type watchEvent struct {
Object runtime.RawExtension `json:"object,omitempty"` 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 // Object converts a watch.Event into an appropriately serializable JSON object
func Object(codec runtime.Codec, event *watch.Event) (interface{}, error) { func Object(codec runtime.Codec, event *watch.Event) (interface{}, error) {
obj, ok := event.Object.(runtime.Object) obj, ok := event.Object.(runtime.Object)