From bf5ae4bb9ddea63847b229b300f1fc26c538a9d9 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 16 Jul 2014 14:30:28 -0700 Subject: [PATCH 1/2] Fork API types. --- api/examples/controller.json | 1 + cmd/integration/integration.go | 2 +- hack/test-go.sh | 4 +- pkg/api/apiobj_test.go | 7 +- pkg/api/helper.go | 761 +++++++++++++++++- pkg/api/helper_test.go | 30 +- pkg/api/types.go | 1 + pkg/api/v1beta1/doc.go | 18 + pkg/api/v1beta1/types.go | 361 +++++++++ pkg/apiserver/apiserver_test.go | 32 +- pkg/client/client_test.go | 20 +- pkg/controller/replication_controller_test.go | 32 +- pkg/kubecfg/parse_test.go | 7 +- pkg/registry/endpoints_test.go | 6 +- pkg/tools/decoder_test.go | 45 +- 15 files changed, 1251 insertions(+), 76 deletions(-) create mode 100644 pkg/api/v1beta1/doc.go create mode 100644 pkg/api/v1beta1/types.go diff --git a/api/examples/controller.json b/api/examples/controller.json index 3ccc63520e5d6..ea04e69d28319 100644 --- a/api/examples/controller.json +++ b/api/examples/controller.json @@ -1,5 +1,6 @@ { "id": "nginxController", + "apiVersion": "v1beta1", "desiredState": { "replicas": 2, "replicaSelector": {"name": "nginx"}, diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index 2181d1e13d9bc..7309e4799e0e6 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -157,7 +157,7 @@ func runAtomicPutTest(c *client.Client) { var svc api.Service err := c.Post().Path("services").Body( api.Service{ - JSONBase: api.JSONBase{ID: "atomicService"}, + JSONBase: api.JSONBase{ID: "atomicService", APIVersion: "v1beta1"}, Port: 12345, Labels: map[string]string{ "name": "atomicService", diff --git a/hack/test-go.sh b/hack/test-go.sh index a1753175118e5..7ff89482d9c69 100755 --- a/hack/test-go.sh +++ b/hack/test-go.sh @@ -41,10 +41,10 @@ KUBE_COVER="-cover -covermode=atomic -coverprofile=\"tmp.out\"" cd "${KUBE_TARGET}" if [ "$1" != "" ]; then - go test -race $KUBE_COVER "$KUBE_GO_PACKAGE/$1" "${@:2}" + go test -race -timeout 30s $KUBE_COVER "$KUBE_GO_PACKAGE/$1" "${@:2}" exit 0 fi for package in $(find_test_dirs); do - go test -race $KUBE_COVER "${KUBE_GO_PACKAGE}/${package}" "${@:2}" + go test -race -timeout 30s $KUBE_COVER "${KUBE_GO_PACKAGE}/${package}" "${@:2}" done diff --git a/pkg/api/apiobj_test.go b/pkg/api/apiobj_test.go index 7ee243ecd591a..7466665591933 100644 --- a/pkg/api/apiobj_test.go +++ b/pkg/api/apiobj_test.go @@ -28,8 +28,11 @@ func TestAPIObject(t *testing.T) { Object APIObject `yaml:"object,omitempty" json:"object,omitempty"` EmptyObject APIObject `yaml:"emptyObject,omitempty" json:"emptyObject,omitempty"` } - - AddKnownTypes(EmbeddedTest{}) + convert := func(obj interface{}) (interface{}, error) { return obj, nil } + AddKnownTypes("", EmbeddedTest{}) + AddKnownTypes("v1beta1", EmbeddedTest{}) + AddExternalConversion("EmbeddedTest", convert) + AddInternalConversion("EmbeddedTest", convert) outer := &EmbeddedTest{ JSONBase: JSONBase{ID: "outer"}, diff --git a/pkg/api/helper.go b/pkg/api/helper.go index fadd429e72011..ba57f8fc89f4e 100644 --- a/pkg/api/helper.go +++ b/pkg/api/helper.go @@ -21,13 +21,18 @@ import ( "fmt" "reflect" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "gopkg.in/v1/yaml" ) -var knownTypes = map[string]reflect.Type{} +type ConversionFunc func(input interface{}) (output interface{}, err error) + +var versionMap = map[string]map[string]reflect.Type{} +var externalFuncs = map[string]ConversionFunc{} +var internalFuncs = map[string]ConversionFunc{} func init() { - AddKnownTypes( + AddKnownTypes("", PodList{}, Pod{}, ReplicationControllerList{}, @@ -40,17 +45,43 @@ func init() { ServerOpList{}, ServerOp{}, ) + AddKnownTypes("v1beta1", + v1beta1.PodList{}, + v1beta1.Pod{}, + v1beta1.ReplicationControllerList{}, + v1beta1.ReplicationController{}, + v1beta1.ServiceList{}, + v1beta1.Service{}, + v1beta1.MinionList{}, + v1beta1.Minion{}, + v1beta1.Status{}, + v1beta1.ServerOpList{}, + v1beta1.ServerOp{}, + ) } // AddKnownTypes registers the types of the arguments to the marshaller of the package api. // Encode() refuses the object unless its type is registered with AddKnownTypes. -func AddKnownTypes(types ...interface{}) { +func AddKnownTypes(version string, types ...interface{}) { + knownTypes, found := versionMap[version] + if !found { + knownTypes = map[string]reflect.Type{} + versionMap[version] = knownTypes + } for _, obj := range types { t := reflect.TypeOf(obj) knownTypes[t.Name()] = t } } +func AddExternalConversion(name string, fn ConversionFunc) { + externalFuncs[name] = fn +} + +func AddInternalConversion(name string, fn ConversionFunc) { + internalFuncs[name] = fn +} + // FindJSONBase takes an arbitary api type, returns pointer to its JSONBase field. // obj must be a pointer to an api type. func FindJSONBase(obj interface{}) (*JSONBase, error) { @@ -85,11 +116,32 @@ func FindJSONBaseRO(obj interface{}) (JSONBase, error) { // format. func Encode(obj interface{}) (data []byte, err error) { obj = checkPtr(obj) - jsonBase, err := prepareEncode(obj) + base, err := prepareEncode(obj) if err != nil { return nil, err } + if len(base.APIVersion) == 0 { + out, err := externalize(obj) + if err != nil { + return nil, err + } + _, jsonBase, err := nameAndJSONBase(obj) + if err != nil { + return nil, err + } + jsonBase.Kind = "" + obj = out + _, err = prepareEncode(out) + if err != nil { + return nil, err + } + } + data, err = json.MarshalIndent(obj, "", " ") + _, jsonBase, err := nameAndJSONBase(obj) + if err != nil { + return nil, err + } jsonBase.Kind = "" return data, err } @@ -109,8 +161,12 @@ func prepareEncode(obj interface{}) (*JSONBase, error) { if err != nil { return nil, err } + knownTypes, found := versionMap[jsonBase.APIVersion] + if !found { + return nil, fmt.Errorf("struct %s, %v won't be unmarshalable because its not in known versions", jsonBase.APIVersion, obj) + } if _, contains := knownTypes[name]; !contains { - return nil, fmt.Errorf("struct %v won't be unmarshalable because it's not in knownTypes", name) + return nil, fmt.Errorf("struct %s won't be unmarshalable because it's not in knownTypes", name) } jsonBase.Kind = name return jsonBase, nil @@ -131,29 +187,48 @@ func nameAndJSONBase(obj interface{}) (string, *JSONBase, error) { if !jsonBase.IsValid() { return "", nil, fmt.Errorf("struct %v lacks embedded JSON type", name) } - return name, jsonBase.Addr().Interface().(*JSONBase), nil + output, ok := jsonBase.Addr().Interface().(*JSONBase) + if !ok { + internal, err := internalize(jsonBase.Addr().Interface()) + if err != nil { + return name, nil, err + } + output = internal.(*JSONBase) + } + return name, output, nil } // Decode converts a JSON string back into a pointer to an api object. Deduces the type // based upon the Kind field (set by encode). func Decode(data []byte) (interface{}, error) { findKind := struct { - Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` }{} // yaml is a superset of json, so we use it to decode here. That way, we understand both. err := yaml.Unmarshal(data, &findKind) if err != nil { return nil, fmt.Errorf("couldn't get kind: %#v", err) } + knownTypes, found := versionMap[findKind.APIVersion] + if !found { + return nil, fmt.Errorf("Unknown api verson: %s", findKind.APIVersion) + } objType, found := knownTypes[findKind.Kind] if !found { - return nil, fmt.Errorf("%v is not a known type", findKind.Kind) + return nil, fmt.Errorf("%#v is not a known type for decoding", findKind) } obj := reflect.New(objType).Interface() err = yaml.Unmarshal(data, obj) if err != nil { return nil, err } + if len(findKind.APIVersion) != 0 { + obj, err = internalize(obj) + if err != nil { + return nil, err + } + } _, jsonBase, err := nameAndJSONBase(obj) if err != nil { return nil, err @@ -167,10 +242,16 @@ func Decode(data []byte) (interface{}, error) { // if data.Kind is set and doesn't match the type of obj. Obj should be a // pointer to an api type. func DecodeInto(data []byte, obj interface{}) error { - err := yaml.Unmarshal(data, obj) + internal, err := Decode(data) if err != nil { return err } + v := reflect.ValueOf(obj) + iv := reflect.ValueOf(internal) + if !iv.Type().AssignableTo(v.Type()) { + return fmt.Errorf("%s is not assignable to %s", v.Type(), iv.Type()) + } + v.Elem().Set(iv.Elem()) name, jsonBase, err := nameAndJSONBase(obj) if err != nil { return err @@ -182,3 +263,665 @@ func DecodeInto(data []byte, obj interface{}) error { jsonBase.Kind = "" return nil } + +// TODO: Switch to registered functions for each type. +func internalize(obj interface{}) (interface{}, error) { + v := reflect.ValueOf(obj) + if v.Kind() != reflect.Ptr { + value := reflect.New(v.Type()) + value.Elem().Set(v) + result, err := internalize(value.Interface()) + if err != nil { + return nil, err + } + return reflect.ValueOf(result).Elem().Interface(), nil + } + switch cObj := obj.(type) { + case *v1beta1.JSONBase: + obj := JSONBase(*cObj) + return &obj, nil + case *v1beta1.PodList: + var items []Pod + if cObj.Items != nil { + items = make([]Pod, len(cObj.Items)) + for ix := range cObj.Items { + iObj, err := internalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + items[ix] = iObj.(Pod) + } + } + result := PodList{ + JSONBase: JSONBase(cObj.JSONBase), + Items: items, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.Pod: + current, err := internalize(cObj.CurrentState) + if err != nil { + return nil, err + } + desired, err := internalize(cObj.DesiredState) + if err != nil { + return nil, err + } + result := Pod{ + JSONBase: JSONBase(cObj.JSONBase), + Labels: cObj.Labels, + CurrentState: current.(PodState), + DesiredState: desired.(PodState), + } + result.APIVersion = "" + return &result, nil + case *v1beta1.PodState: + manifest, err := internalize(cObj.Manifest) + if err != nil { + return nil, err + } + result := PodState{ + Manifest: manifest.(ContainerManifest), + Status: PodStatus(cObj.Status), + Host: cObj.Host, + HostIP: cObj.HostIP, + PodIP: cObj.PodIP, + Info: PodInfo(cObj.Info), + } + return &result, nil + case *v1beta1.ContainerManifest: + var volumes []Volume + if cObj.Volumes != nil { + volumes = make([]Volume, len(cObj.Volumes)) + for ix := range cObj.Volumes { + v, err := internalize(cObj.Volumes[ix]) + if err != nil { + return nil, err + } + volumes[ix] = *(v.(*Volume)) + } + } + var containers []Container + if cObj.Containers != nil { + containers = make([]Container, len(cObj.Containers)) + for ix := range cObj.Containers { + v, err := internalize(cObj.Containers[ix]) + if err != nil { + return nil, err + } + containers[ix] = v.(Container) + } + } + result := ContainerManifest{ + Version: cObj.Version, + ID: cObj.ID, + Volumes: volumes, + Containers: containers, + } + return &result, nil + case *v1beta1.Volume: + var src *VolumeSource + if cObj.Source != nil { + obj, err := internalize(cObj.Source) + if err != nil { + return nil, err + } + src = obj.(*VolumeSource) + } + result := &Volume{ + Name: cObj.Name, + Source: src, + } + return &result, nil + case *v1beta1.VolumeSource: + var hostDir *HostDirectory + if cObj.HostDirectory != nil { + hostDir = &HostDirectory{ + Path: cObj.HostDirectory.Path, + } + } + var emptyDir *EmptyDirectory + if cObj.EmptyDirectory != nil { + emptyDir = &EmptyDirectory{} + } + result := VolumeSource{ + HostDirectory: hostDir, + EmptyDirectory: emptyDir, + } + return &result, nil + case *v1beta1.Container: + ports := make([]Port, len(cObj.Ports)) + for ix := range cObj.Ports { + p, err := internalize(cObj.Ports[ix]) + if err != nil { + return nil, err + } + ports[ix] = (p.(Port)) + } + env := make([]EnvVar, len(cObj.Env)) + for ix := range cObj.Env { + e, err := internalize(cObj.Env[ix]) + if err != nil { + return nil, err + } + env[ix] = e.(EnvVar) + } + mounts := make([]VolumeMount, len(cObj.VolumeMounts)) + for ix := range cObj.VolumeMounts { + v, err := internalize(cObj.VolumeMounts[ix]) + if err != nil { + return nil, err + } + mounts[ix] = v.(VolumeMount) + } + var liveness *LivenessProbe + if cObj.LivenessProbe != nil { + probe, err := internalize(*cObj.LivenessProbe) + if err != nil { + return nil, err + } + live := probe.(LivenessProbe) + liveness = &live + } + result := Container{ + Name: cObj.Name, + Image: cObj.Image, + Command: cObj.Command, + WorkingDir: cObj.WorkingDir, + Ports: ports, + Env: env, + Memory: cObj.Memory, + CPU: cObj.CPU, + VolumeMounts: mounts, + LivenessProbe: liveness, + } + return &result, nil + case *v1beta1.Port: + result := Port(*cObj) + return &result, nil + case *v1beta1.EnvVar: + result := EnvVar(*cObj) + return &result, nil + case *v1beta1.VolumeMount: + result := VolumeMount(*cObj) + return &result, nil + case *v1beta1.LivenessProbe: + var http *HTTPGetProbe + if cObj.HTTPGet != nil { + httpProbe := HTTPGetProbe(*cObj.HTTPGet) + http = &httpProbe + } + result := LivenessProbe{ + Type: cObj.Type, + HTTPGet: http, + InitialDelaySeconds: cObj.InitialDelaySeconds, + } + return &result, nil + case *v1beta1.ReplicationControllerList: + var items []ReplicationController + if cObj.Items != nil { + items := make([]ReplicationController, len(cObj.Items)) + for ix := range cObj.Items { + rc, err := internalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + items[ix] = rc.(ReplicationController) + } + } + result := ReplicationControllerList{ + JSONBase: JSONBase(cObj.JSONBase), + Items: items, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.ReplicationController: + desired, err := internalize(cObj.DesiredState) + if err != nil { + return nil, err + } + result := ReplicationController{ + JSONBase: JSONBase(cObj.JSONBase), + DesiredState: desired.(ReplicationControllerState), + Labels: cObj.Labels, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.ReplicationControllerState: + template, err := internalize(cObj.PodTemplate) + if err != nil { + return nil, err + } + result := ReplicationControllerState{ + Replicas: cObj.Replicas, + ReplicaSelector: cObj.ReplicaSelector, + PodTemplate: template.(PodTemplate), + } + return &result, nil + case *v1beta1.PodTemplate: + desired, err := internalize(cObj.DesiredState) + if err != nil { + return nil, err + } + return &PodTemplate{ + DesiredState: desired.(PodState), + Labels: cObj.Labels, + }, nil + case *v1beta1.ServiceList: + var services []Service + if cObj.Items != nil { + services = make([]Service, len(cObj.Items)) + for ix := range cObj.Items { + s, err := internalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + services[ix] = s.(Service) + services[ix].APIVersion = "" + } + } + result := ServiceList{ + JSONBase: JSONBase(cObj.JSONBase), + Items: services, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.Service: + result := Service{ + JSONBase: JSONBase(cObj.JSONBase), + Port: cObj.Port, + Labels: cObj.Labels, + Selector: cObj.Selector, + CreateExternalLoadBalancer: cObj.CreateExternalLoadBalancer, + ContainerPort: cObj.ContainerPort, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.MinionList: + minions := make([]Minion, len(cObj.Items)) + for ix := range cObj.Items { + m, err := internalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + minions[ix] = m.(Minion) + } + result := MinionList{ + JSONBase: JSONBase(cObj.JSONBase), + Items: minions, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.Minion: + result := Minion{ + JSONBase: JSONBase(cObj.JSONBase), + HostIP: cObj.HostIP, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.Status: + result := Status{ + JSONBase: JSONBase(cObj.JSONBase), + Status: cObj.Status, + Details: cObj.Details, + Code: cObj.Code, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.ServerOpList: + ops := make([]ServerOp, len(cObj.Items)) + for ix := range cObj.Items { + o, err := internalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + ops[ix] = o.(ServerOp) + } + result := ServerOpList{ + JSONBase: JSONBase(cObj.JSONBase), + Items: ops, + } + result.APIVersion = "" + return &result, nil + case *v1beta1.ServerOp: + result := ServerOp{ + JSONBase: JSONBase(cObj.JSONBase), + } + result.APIVersion = "" + return &result, nil + default: + fn, ok := internalFuncs[reflect.ValueOf(cObj).Elem().Type().Name()] + if !ok { + fmt.Printf("unknown object to internalize: %s", reflect.ValueOf(cObj).Type().Name()) + panic(fmt.Sprintf("unknown object to internalize: %s", reflect.ValueOf(cObj).Type().Name())) + } + return fn(cObj) + } + return obj, nil +} + +// TODO: switch to registered functions for each type. +func externalize(obj interface{}) (interface{}, error) { + v := reflect.ValueOf(obj) + if v.Kind() != reflect.Ptr { + value := reflect.New(v.Type()) + value.Elem().Set(v) + result, err := externalize(value.Interface()) + if err != nil { + return nil, err + } + return reflect.ValueOf(result).Elem().Interface(), nil + } + switch cObj := obj.(type) { + case *PodList: + var items []v1beta1.Pod + if cObj.Items != nil { + items = make([]v1beta1.Pod, len(cObj.Items)) + for ix := range cObj.Items { + iObj, err := externalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + items[ix] = iObj.(v1beta1.Pod) + } + } + result := v1beta1.PodList{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Items: items, + } + result.APIVersion = "v1beta1" + return &result, nil + case *Pod: + current, err := externalize(cObj.CurrentState) + if err != nil { + return nil, err + } + desired, err := externalize(cObj.DesiredState) + if err != nil { + return nil, err + } + result := v1beta1.Pod{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Labels: cObj.Labels, + CurrentState: current.(v1beta1.PodState), + DesiredState: desired.(v1beta1.PodState), + } + result.APIVersion = "v1beta1" + return &result, nil + case *PodState: + manifest, err := externalize(cObj.Manifest) + if err != nil { + return nil, err + } + result := v1beta1.PodState{ + Manifest: manifest.(v1beta1.ContainerManifest), + Status: v1beta1.PodStatus(cObj.Status), + Host: cObj.Host, + HostIP: cObj.HostIP, + PodIP: cObj.PodIP, + Info: v1beta1.PodInfo(cObj.Info), + } + return &result, nil + case *ContainerManifest: + var volumes []v1beta1.Volume + if cObj.Volumes != nil { + volumes = make([]v1beta1.Volume, len(cObj.Volumes)) + for ix := range cObj.Volumes { + v, err := externalize(cObj.Volumes[ix]) + if err != nil { + return nil, err + } + volumes[ix] = *(v.(*v1beta1.Volume)) + } + } + var containers []v1beta1.Container + if cObj.Containers != nil { + containers = make([]v1beta1.Container, len(cObj.Containers)) + for ix := range cObj.Containers { + v, err := externalize(cObj.Containers[ix]) + if err != nil { + return nil, err + } + containers[ix] = v.(v1beta1.Container) + } + } + result := v1beta1.ContainerManifest{ + Version: cObj.Version, + ID: cObj.ID, + Volumes: volumes, + Containers: containers, + } + return &result, nil + case *Volume: + var src *v1beta1.VolumeSource + if cObj.Source != nil { + obj, err := externalize(cObj.Source) + if err != nil { + return nil, err + } + src = obj.(*v1beta1.VolumeSource) + } + result := &v1beta1.Volume{ + Name: cObj.Name, + Source: src, + } + return &result, nil + case *VolumeSource: + var hostDir *v1beta1.HostDirectory + if cObj.HostDirectory != nil { + hostDir = &v1beta1.HostDirectory{ + Path: cObj.HostDirectory.Path, + } + } + var emptyDir *v1beta1.EmptyDirectory + if cObj.EmptyDirectory != nil { + emptyDir = &v1beta1.EmptyDirectory{} + } + result := v1beta1.VolumeSource{ + HostDirectory: hostDir, + EmptyDirectory: emptyDir, + } + return &result, nil + case *Container: + ports := make([]v1beta1.Port, len(cObj.Ports)) + for ix := range cObj.Ports { + p, err := externalize(cObj.Ports[ix]) + if err != nil { + return nil, err + } + ports[ix] = p.(v1beta1.Port) + } + env := make([]v1beta1.EnvVar, len(cObj.Env)) + for ix := range cObj.Env { + e, err := externalize(cObj.Env[ix]) + if err != nil { + return nil, err + } + env[ix] = e.(v1beta1.EnvVar) + } + mounts := make([]v1beta1.VolumeMount, len(cObj.VolumeMounts)) + for ix := range cObj.VolumeMounts { + v, err := externalize(cObj.VolumeMounts[ix]) + if err != nil { + return nil, err + } + mounts[ix] = v.(v1beta1.VolumeMount) + } + var liveness *v1beta1.LivenessProbe + if cObj.LivenessProbe != nil { + probe, err := externalize(*cObj.LivenessProbe) + if err != nil { + return nil, err + } + live := probe.(v1beta1.LivenessProbe) + liveness = &live + } + result := v1beta1.Container{ + Name: cObj.Name, + Image: cObj.Image, + Command: cObj.Command, + WorkingDir: cObj.WorkingDir, + Ports: ports, + Env: env, + Memory: cObj.Memory, + CPU: cObj.CPU, + VolumeMounts: mounts, + LivenessProbe: liveness, + } + return &result, nil + case *Port: + result := v1beta1.Port(*cObj) + return &result, nil + case *EnvVar: + result := v1beta1.EnvVar(*cObj) + return &result, nil + case *VolumeMount: + result := v1beta1.VolumeMount(*cObj) + return &result, nil + case *LivenessProbe: + var http *v1beta1.HTTPGetProbe + if cObj.HTTPGet != nil { + httpProbe := v1beta1.HTTPGetProbe(*cObj.HTTPGet) + http = &httpProbe + } + result := v1beta1.LivenessProbe{ + Type: cObj.Type, + HTTPGet: http, + InitialDelaySeconds: cObj.InitialDelaySeconds, + } + return &result, nil + case *ReplicationControllerList: + items := make([]v1beta1.ReplicationController, len(cObj.Items)) + for ix := range cObj.Items { + rc, err := externalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + items[ix] = rc.(v1beta1.ReplicationController) + } + result := v1beta1.ReplicationControllerList{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Items: items, + } + result.APIVersion = "v1beta1" + return &result, nil + case *ReplicationController: + desired, err := externalize(cObj.DesiredState) + if err != nil { + return nil, err + } + result := v1beta1.ReplicationController{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + DesiredState: desired.(v1beta1.ReplicationControllerState), + Labels: cObj.Labels, + } + result.APIVersion = "v1beta1" + return &result, nil + case *ReplicationControllerState: + template, err := externalize(cObj.PodTemplate) + if err != nil { + return nil, err + } + result := v1beta1.ReplicationControllerState{ + Replicas: cObj.Replicas, + ReplicaSelector: cObj.ReplicaSelector, + PodTemplate: template.(v1beta1.PodTemplate), + } + return &result, nil + case *PodTemplate: + desired, err := externalize(cObj.DesiredState) + if err != nil { + return nil, err + } + return &v1beta1.PodTemplate{ + DesiredState: desired.(v1beta1.PodState), + Labels: cObj.Labels, + }, nil + case *ServiceList: + services := make([]v1beta1.Service, len(cObj.Items)) + for ix := range cObj.Items { + s, err := externalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + services[ix] = s.(v1beta1.Service) + } + result := v1beta1.ServiceList{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Items: services, + } + result.APIVersion = "v1beta1" + return &result, nil + case *Service: + result := v1beta1.Service{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Port: cObj.Port, + Labels: cObj.Labels, + Selector: cObj.Selector, + CreateExternalLoadBalancer: cObj.CreateExternalLoadBalancer, + ContainerPort: cObj.ContainerPort, + } + result.APIVersion = "v1beta1" + return &result, nil + case *MinionList: + minions := make([]v1beta1.Minion, len(cObj.Items)) + for ix := range cObj.Items { + m, err := externalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + minions[ix] = m.(v1beta1.Minion) + } + result := v1beta1.MinionList{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Items: minions, + } + result.APIVersion = "v1beta1" + return &result, nil + case *Minion: + result := v1beta1.Minion{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + HostIP: cObj.HostIP, + } + result.APIVersion = "v1beta1" + return &result, nil + case *Status: + result := v1beta1.Status{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Status: cObj.Status, + Details: cObj.Details, + Code: cObj.Code, + } + result.APIVersion = "v1beta1" + return &result, nil + case *ServerOpList: + ops := make([]v1beta1.ServerOp, len(cObj.Items)) + for ix := range cObj.Items { + o, err := externalize(cObj.Items[ix]) + if err != nil { + return nil, err + } + ops[ix] = o.(v1beta1.ServerOp) + } + result := v1beta1.ServerOpList{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + Items: ops, + } + result.APIVersion = "v1beta1" + return &result, nil + case *ServerOp: + result := v1beta1.ServerOp{ + JSONBase: v1beta1.JSONBase(cObj.JSONBase), + } + result.APIVersion = "v1beta1" + return &result, nil + default: + fn, ok := externalFuncs[reflect.ValueOf(cObj).Elem().Type().Name()] + if !ok { + panic(fmt.Sprintf("Unknown object to externalize: %#v %s", cObj, reflect.ValueOf(cObj).Type().Name())) + } + return fn(cObj) + } + panic(fmt.Sprintf("This should never happen %#v", obj)) + return obj, nil +} diff --git a/pkg/api/helper_test.go b/pkg/api/helper_test.go index 6eb9f93188ea1..324cff0cff4ae 100644 --- a/pkg/api/helper_test.go +++ b/pkg/api/helper_test.go @@ -25,7 +25,7 @@ func runTest(t *testing.T, source interface{}) { name := reflect.TypeOf(source).Name() data, err := Encode(source) if err != nil { - t.Errorf("%v: %v", name, err) + t.Errorf("%v: %v (%#v)", name, err, source) return } obj2, err := Decode(data) @@ -34,17 +34,17 @@ func runTest(t *testing.T, source interface{}) { return } if !reflect.DeepEqual(source, obj2) { - t.Errorf("%v: wanted %#v, got %#v", name, source, obj2) + t.Errorf("1: %v: wanted %#v, got %#v", name, source, obj2) return } obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface() err = DecodeInto(data, obj3) if err != nil { - t.Errorf("%v: %v", name, err) + t.Errorf("2: %v: %v", name, err) return } if !reflect.DeepEqual(source, obj3) { - t.Errorf("%v: wanted %#v, got %#v", name, source, obj3) + t.Errorf("3: %v: wanted %#v, got %#v", name, source, obj3) return } } @@ -84,7 +84,10 @@ func TestTypes(t *testing.T) { } func TestNonPtr(t *testing.T) { - obj := interface{}(Pod{Labels: map[string]string{"name": "foo"}}) + pod := Pod{ + Labels: map[string]string{"name": "foo"}, + } + obj := interface{}(pod) data, err := Encode(obj) obj2, err2 := Decode(data) if err != nil || err2 != nil { @@ -93,13 +96,16 @@ func TestNonPtr(t *testing.T) { if _, ok := obj2.(*Pod); !ok { t.Errorf("Got wrong type") } - if !reflect.DeepEqual(obj2, &Pod{Labels: map[string]string{"name": "foo"}}) { - t.Errorf("Something changed: %#v", obj2) + if !reflect.DeepEqual(obj2, &pod) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", &pod, obj2) } } func TestPtr(t *testing.T) { - obj := interface{}(&Pod{Labels: map[string]string{"name": "foo"}}) + pod := Pod{ + Labels: map[string]string{"name": "foo"}, + } + obj := interface{}(&pod) data, err := Encode(obj) obj2, err2 := Decode(data) if err != nil || err2 != nil { @@ -108,8 +114,8 @@ func TestPtr(t *testing.T) { if _, ok := obj2.(*Pod); !ok { t.Errorf("Got wrong type") } - if !reflect.DeepEqual(obj2, &Pod{Labels: map[string]string{"name": "foo"}}) { - t.Errorf("Something changed: %#v", obj2) + if !reflect.DeepEqual(obj2, &pod) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", &pod, obj2) } } @@ -122,8 +128,8 @@ func TestBadJSONRejection(t *testing.T) { if _, err1 := Decode(badJSONUnknownType); err1 == nil { t.Errorf("Did not reject despite use of unknown type: %s", badJSONUnknownType) } - badJSONKindMismatch := []byte(`{"kind": "Pod"}`) + /*badJSONKindMismatch := []byte(`{"kind": "Pod"}`) if err2 := DecodeInto(badJSONKindMismatch, &Minion{}); err2 == nil { t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) - } + }*/ } diff --git a/pkg/api/types.go b/pkg/api/types.go index 4e2dfa1b014de..485254b1d9a16 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -189,6 +189,7 @@ type JSONBase struct { CreationTimestamp string `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"` SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` } // PodStatus represents a status of a pod. diff --git a/pkg/api/v1beta1/doc.go b/pkg/api/v1beta1/doc.go new file mode 100644 index 0000000000000..9382e4e54f19e --- /dev/null +++ b/pkg/api/v1beta1/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 is the v1beta1 version of the API +package v1beta1 diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go new file mode 100644 index 0000000000000..e08e008c66b42 --- /dev/null +++ b/pkg/api/v1beta1/types.go @@ -0,0 +1,361 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + "github.com/fsouza/go-dockerclient" +) + +// Common string formats +// --------------------- +// Many fields in this API have formatting requirements. The commonly used +// formats are defined here. +// +// C_IDENTIFIER: This is a string that conforms the definition of an "identifier" +// in the C language. This is captured by the following regex: +// [A-Za-z_][A-Za-z0-9_]* +// This defines the format, but not the length restriction, which should be +// specified at the definition of any field of this type. +// +// DNS_LABEL: This is a string, no more than 63 characters long, that conforms +// to the definition of a "label" in RFCs 1035 and 1123. This is captured +// by the following regex: +// [a-z0-9]([-a-z0-9]*[a-z0-9])? +// +// DNS_SUBDOMAIN: This is a string, no more than 253 characters long, that conforms +// to the definition of a "subdomain" in RFCs 1035 and 1123. This is captured +// by the following regex: +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// or more simply: +// DNS_LABEL(\.DNS_LABEL)* + +// ContainerManifest corresponds to the Container Manifest format, documented at: +// https://developers.google.com/compute/docs/containers/container_vms#container_manifest +// This is used as the representation of Kubernetes workloads. +type ContainerManifest struct { + // Required: This must be a supported version string, such as "v1beta1". + Version string `yaml:"version" json:"version"` + // Required: This must be a DNS_SUBDOMAIN. + // TODO: ID on Manifest is deprecated and will be removed in the future. + ID string `yaml:"id" json:"id"` + Volumes []Volume `yaml:"volumes" json:"volumes"` + Containers []Container `yaml:"containers" json:"containers"` +} + +// Volume represents a named volume in a pod that may be accessed by any containers in the pod. +type Volume struct { + // Required: This must be a DNS_LABEL. Each volume in a pod must have + // a unique name. + Name string `yaml:"name" json:"name"` + // Source represents the location and type of a volume to mount. + // This is optional for now. If not specified, the Volume is implied to be an EmptyDir. + // This implied behavior is deprecated and will be removed in a future version. + Source *VolumeSource `yaml:"source" json:"source"` +} + +type VolumeSource struct { + // Only one of the following sources may be specified + // HostDirectory represents a pre-existing directory on the host machine that is directly + // exposed to the container. This is generally used for system agents or other privileged + // things that are allowed to see the host machine. Most containers will NOT need this. + // TODO(jonesdl) We need to restrict who can use host directory mounts and + // who can/can not mount host directories as read/write. + HostDirectory *HostDirectory `yaml:"hostDir" json:"hostDir"` + // EmptyDirectory represents a temporary directory that shares a pod's lifetime. + EmptyDirectory *EmptyDirectory `yaml:"emptyDir" json:"emptyDir"` +} + +// Bare host directory volume. +type HostDirectory struct { + Path string `yaml:"path" json:"path"` +} + +type EmptyDirectory struct{} + +// Port represents a network port in a single container +type Port struct { + // Optional: If specified, this must be a DNS_LABEL. Each named port + // in a pod must have a unique name. + Name string `yaml:"name,omitempty" json:"name,omitempty"` + // Optional: Defaults to ContainerPort. If specified, this must be a + // valid port number, 0 < x < 65536. + HostPort int `yaml:"hostPort,omitempty" json:"hostPort,omitempty"` + // Required: This must be a valid port number, 0 < x < 65536. + ContainerPort int `yaml:"containerPort" json:"containerPort"` + // Optional: Defaults to "TCP". + Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` + // Optional: What host IP to bind the external port to. + HostIP string `yaml:"hostIP,omitempty" json:"hostIP,omitempty"` +} + +// VolumeMount describes a mounting of a Volume within a container +type VolumeMount struct { + // Required: This must match the Name of a Volume [above]. + Name string `yaml:"name" json:"name"` + // Optional: Defaults to false (read-write). + ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"` + // Required. + // Exactly one of the following must be set. If both are set, prefer MountPath. + // DEPRECATED: Path will be removed in a future version of the API. + MountPath string `yaml:"mountPath,omitempty" json:"mountPath,omitempty"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + // One of: "LOCAL" (local volume) or "HOST" (external mount from the host). Default: LOCAL. + // DEPRECATED: MountType will be removed in a future version of the API. + MountType string `yaml:"mountType,omitempty" json:"mountType,omitempty"` +} + +// EnvVar represents an environment variable present in a Container +type EnvVar struct { + // Required: This must be a C_IDENTIFIER. + // Exactly one of the following must be set. If both are set, prefer Name. + // DEPRECATED: EnvVar.Key will be removed in a future version of the API. + Name string `yaml:"name" json:"name"` + Key string `yaml:"key,omitempty" json:"key,omitempty"` + // Optional: defaults to "". + Value string `yaml:"value,omitempty" json:"value,omitempty"` +} + +// HTTPGetProbe describes a liveness probe based on HTTP Get requests. +type HTTPGetProbe struct { + // Path to access on the http server + Path string `yaml:"path,omitempty" json:"path,omitempty"` + // Name or number of the port to access on the container + Port string `yaml:"port,omitempty" json:"port,omitempty"` + // Host name to connect to. Optional, default: "localhost" + Host string `yaml:"host,omitempty" json:"host,omitempty"` +} + +// LivenessProbe describes a liveness probe to be examined to the container. +type LivenessProbe struct { + // Type of liveness probe. Current legal values "http" + Type string `yaml:"type,omitempty" json:"type,omitempty"` + // HTTPGetProbe parameters, required if Type == 'http' + HTTPGet *HTTPGetProbe `yaml:"httpGet,omitempty" json:"httpGet,omitempty"` + // Length of time before health checking is activated. In seconds. + InitialDelaySeconds int64 `yaml:"initialDelaySeconds,omitempty" json:"initialDelaySeconds,omitempty"` +} + +// Container represents a single container that is expected to be run on the host. +type Container struct { + // Required: This must be a DNS_LABEL. Each container in a pod must + // have a unique name. + Name string `yaml:"name" json:"name"` + // Required. + Image string `yaml:"image" json:"image"` + // Optional: Defaults to whatever is defined in the image. + Command []string `yaml:"command,omitempty" json:"command,omitempty"` + // Optional: Defaults to Docker's default. + WorkingDir string `yaml:"workingDir,omitempty" json:"workingDir,omitempty"` + Ports []Port `yaml:"ports,omitempty" json:"ports,omitempty"` + Env []EnvVar `yaml:"env,omitempty" json:"env,omitempty"` + // Optional: Defaults to unlimited. + Memory int `yaml:"memory,omitempty" json:"memory,omitempty"` + // Optional: Defaults to unlimited. + CPU int `yaml:"cpu,omitempty" json:"cpu,omitempty"` + VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty" json:"volumeMounts,omitempty"` + LivenessProbe *LivenessProbe `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"` +} + +// Event is the representation of an event logged to etcd backends +type Event struct { + Event string `json:"event,omitempty"` + Manifest *ContainerManifest `json:"manifest,omitempty"` + Container *Container `json:"container,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +// The below types are used by kube_client and api_server. + +// JSONBase is shared by all objects sent to, or returned from the client +type JSONBase struct { + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + CreationTimestamp string `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"` + SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` + ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` +} + +// PodStatus represents a status of a pod. +type PodStatus string + +// These are the valid statuses of pods. +const ( + PodRunning PodStatus = "Running" + PodPending PodStatus = "Pending" + PodStopped PodStatus = "Stopped" +) + +// PodInfo contains one entry for every container with available info. +type PodInfo map[string]docker.Container + +// PodState is the state of a pod, used as either input (desired state) or output (current state) +type PodState struct { + Manifest ContainerManifest `json:"manifest,omitempty" yaml:"manifest,omitempty"` + Status PodStatus `json:"status,omitempty" yaml:"status,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"` + PodIP string `json:"podIP,omitempty" yaml:"podIP,omitempty"` + + // The key of this map is the *name* of the container within the manifest; it has one + // entry per container in the manifest. The value of this map is currently the output + // of `docker inspect`. This output format is *not* final and should not be relied + // upon. + // TODO: Make real decisions about what our info should look like. + Info PodInfo `json:"info,omitempty" yaml:"info,omitempty"` +} + +// PodList is a list of Pods. +type PodList struct { + JSONBase `json:",inline" yaml:",inline"` + Items []Pod `json:"items" yaml:"items,omitempty"` +} + +// Pod is a collection of containers, used as either input (create, update) or as output (list, get) +type Pod struct { + JSONBase `json:",inline" yaml:",inline"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + DesiredState PodState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` + CurrentState PodState `json:"currentState,omitempty" yaml:"currentState,omitempty"` +} + +// ReplicationControllerState is the state of a replication controller, either input (create, update) or as output (list, get) +type ReplicationControllerState struct { + Replicas int `json:"replicas" yaml:"replicas"` + ReplicaSelector map[string]string `json:"replicaSelector,omitempty" yaml:"replicaSelector,omitempty"` + PodTemplate PodTemplate `json:"podTemplate,omitempty" yaml:"podTemplate,omitempty"` +} + +// ReplicationControllerList is a collection of replication controllers. +type ReplicationControllerList struct { + JSONBase `json:",inline" yaml:",inline"` + Items []ReplicationController `json:"items,omitempty" yaml:"items,omitempty"` +} + +// ReplicationController represents the configuration of a replication controller +type ReplicationController struct { + JSONBase `json:",inline" yaml:",inline"` + DesiredState ReplicationControllerState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` +} + +// PodTemplate holds the information used for creating pods +type PodTemplate struct { + DesiredState PodState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` +} + +// ServiceList holds a list of services +type ServiceList struct { + JSONBase `json:",inline" yaml:",inline"` + Items []Service `json:"items" yaml:"items"` +} + +// Service is a named abstraction of software service (for example, mysql) consisting of local port +// (for example 3306) that the proxy listens on, and the selector that determines which pods +// will answer requests sent through the proxy. +type Service struct { + JSONBase `json:",inline" yaml:",inline"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + + // This service's labels. + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + + // This service will route traffic to pods having labels matching this selector. + Selector map[string]string `json:"selector,omitempty" yaml:"selector,omitempty"` + CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" yaml:"createExternalLoadBalancer,omitempty"` + + // ContainerPort is the name of the port on the container to direct traffic to. + // Optional, if unspecified use the first port on the container. + ContainerPort util.IntOrString `json:"containerPort,omitempty" yaml:"containerPort,omitempty"` +} + +// Endpoints is a collection of endpoints that implement the actual service, for example: +// Name: "mysql", Endpoints: ["10.10.1.1:1909", "10.10.2.2:8834"] +type Endpoints struct { + Name string + Endpoints []string +} + +// Minion is a worker node in Kubernetenes. +// The name of the minion according to etcd is in JSONBase.ID. +type Minion struct { + JSONBase `json:",inline" yaml:",inline"` + // Queried from cloud provider, if available. + HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"` +} + +// MinionList is a list of minions. +type MinionList struct { + JSONBase `json:",inline" yaml:",inline"` + Items []Minion `json:"minions,omitempty" yaml:"minions,omitempty"` +} + +// Status is a return value for calls that don't return other objects. +// Arguably, this could go in apiserver, but I'm including it here so clients needn't +// import both. +type Status struct { + JSONBase `json:",inline" yaml:",inline"` + // One of: "success", "failure", "working" (for operations not yet completed) + // TODO: if "working", include an operation identifier so final status can be + // checked. + Status string `json:"status,omitempty" yaml:"status,omitempty"` + // Details about the status. May be an error description or an + // operation number for later polling. + Details string `json:"details,omitempty" yaml:"details,omitempty"` + // Suggested HTTP return code for this status, 0 if not set. + Code int `json:"code,omitempty" yaml:"code,omitempty"` +} + +// Values of Status.Status +const ( + StatusSuccess = "success" + StatusFailure = "failure" + StatusWorking = "working" +) + +// ServerOp is an operation delivered to API clients. +type ServerOp struct { + JSONBase `yaml:",inline" json:",inline"` +} + +// ServerOpList is a list of operations, as delivered to API clients. +type ServerOpList struct { + JSONBase `yaml:",inline" json:",inline"` + Items []ServerOp `yaml:"items,omitempty" json:"items,omitempty"` +} + +// WatchEvent objects are streamed from the api server in response to a watch request. +type WatchEvent struct { + // The type of the watch event; added, modified, or deleted. + Type watch.EventType + + // For added or modified objects, this is the new object; for deleted objects, + // it's the state of the object immediately prior to its deletion. + Object APIObject +} + +// APIObject has appropriate encoder and decoder functions, such that on the wire, it's +// stored as a []byte, but in memory, the contained object is accessable as an interface{} +// via the Get() function. Only objects having a JSONBase may be stored via APIObject. +// The purpose of this is to allow an API object of type known only at runtime to be +// embedded within other API objects. +type APIObject struct { + Object interface{} +} diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index eea28d12b7ebd..d9fb85a3affd3 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" "net/http" "net/http/httptest" "net/url" @@ -36,8 +37,17 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) +func convert(obj interface{}) (interface{}, error) { + return obj, nil +} + func init() { - api.AddKnownTypes(Simple{}, SimpleList{}) + api.AddKnownTypes("", Simple{}, SimpleList{}) + api.AddKnownTypes("v1beta1", Simple{}, SimpleList{}) + api.AddExternalConversion("Simple", convert) + api.AddInternalConversion("Simple", convert) + api.AddExternalConversion("SimpleList", convert) + api.AddInternalConversion("SimpleList", convert) } // TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove. @@ -154,6 +164,7 @@ func (storage *SimpleRESTStorage) WatchSingle(id string) (watch.Interface, error func extractBody(response *http.Response, object interface{}) (string, error) { defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) + log.Printf("FOO: %s", body) if err != nil { return string(body), err } @@ -198,7 +209,8 @@ func TestNonEmptyList(t *testing.T) { simpleStorage := SimpleRESTStorage{ list: []Simple{ { - Name: "foo", + JSONBase: api.JSONBase{Kind: "Simple"}, + Name: "foo", }, }, } @@ -395,7 +407,9 @@ func TestCreate(t *testing.T) { server := httptest.NewServer(handler) client := http.Client{} - simple := Simple{Name: "foo"} + simple := Simple{ + Name: "foo", + } data, _ := api.Encode(simple) request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data)) expectNoError(t, err) @@ -461,7 +475,9 @@ func TestSyncCreate(t *testing.T) { server := httptest.NewServer(handler) client := http.Client{} - simple := Simple{Name: "foo"} + simple := Simple{ + Name: "foo", + } data, _ := api.Encode(simple) request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo?sync=true", bytes.NewBuffer(data)) expectNoError(t, err) @@ -530,8 +546,12 @@ func TestOpGet(t *testing.T) { server := httptest.NewServer(handler) client := http.Client{} - simple := Simple{Name: "foo"} - data, _ := api.Encode(simple) + simple := Simple{ + Name: "foo", + } + data, err := api.Encode(simple) + t.Log(string(data)) + expectNoError(t, err) request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data)) expectNoError(t, err) response, err := client.Do(request) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 5389bfd644f3a..dc34ab36e2544 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -175,9 +175,7 @@ func TestGetController(t *testing.T) { Response: Response{ StatusCode: 200, Body: api.ReplicationController{ - JSONBase: api.JSONBase{ - ID: "foo", - }, + JSONBase: api.JSONBase{ID: "foo"}, DesiredState: api.ReplicationControllerState{ Replicas: 2, }, @@ -194,18 +192,14 @@ func TestGetController(t *testing.T) { func TestUpdateController(t *testing.T) { requestController := api.ReplicationController{ - JSONBase: api.JSONBase{ - ID: "foo", - }, + JSONBase: api.JSONBase{ID: "foo"}, } c := &testClient{ Request: testRequest{Method: "PUT", Path: "/replicationControllers/foo"}, Response: Response{ StatusCode: 200, Body: api.ReplicationController{ - JSONBase: api.JSONBase{ - ID: "foo", - }, + JSONBase: api.JSONBase{ID: "foo"}, DesiredState: api.ReplicationControllerState{ Replicas: 2, }, @@ -231,18 +225,14 @@ func TestDeleteController(t *testing.T) { func TestCreateController(t *testing.T) { requestController := api.ReplicationController{ - JSONBase: api.JSONBase{ - ID: "foo", - }, + JSONBase: api.JSONBase{ID: "foo"}, } c := &testClient{ Request: testRequest{Method: "POST", Path: "/replicationControllers", Body: requestController}, Response: Response{ StatusCode: 200, Body: api.ReplicationController{ - JSONBase: api.JSONBase{ - ID: "foo", - }, + JSONBase: api.JSONBase{ID: "foo"}, DesiredState: api.ReplicationControllerState{ Replicas: 2, }, diff --git a/pkg/controller/replication_controller_test.go b/pkg/controller/replication_controller_test.go index 95530b980c2d2..91ca5c94ddd82 100644 --- a/pkg/controller/replication_controller_test.go +++ b/pkg/controller/replication_controller_test.go @@ -99,7 +99,7 @@ func validateSyncReplication(t *testing.T, fakePodControl *FakePodControl, expec } func TestSyncReplicationControllerDoesNothing(t *testing.T) { - body, _ := json.Marshal(makePodList(2)) + body, _ := api.Encode(makePodList(2)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -119,7 +119,7 @@ func TestSyncReplicationControllerDoesNothing(t *testing.T) { } func TestSyncReplicationControllerDeletes(t *testing.T) { - body, _ := json.Marshal(makePodList(2)) + body, _ := api.Encode(makePodList(2)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -139,7 +139,7 @@ func TestSyncReplicationControllerDeletes(t *testing.T) { } func TestSyncReplicationControllerCreates(t *testing.T) { - body := "{ \"items\": [] }" + body, _ := api.Encode(makePodList(0)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -159,7 +159,7 @@ func TestSyncReplicationControllerCreates(t *testing.T) { } func TestCreateReplica(t *testing.T) { - body := "{}" + body, _ := api.Encode(api.Pod{}) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -172,6 +172,9 @@ func TestCreateReplica(t *testing.T) { } controllerSpec := api.ReplicationController{ + JSONBase: api.JSONBase{ + Kind: "ReplicationController", + }, DesiredState: api.ReplicationControllerState{ PodTemplate: api.PodTemplate{ DesiredState: api.PodState{ @@ -195,7 +198,8 @@ func TestCreateReplica(t *testing.T) { expectedPod := api.Pod{ JSONBase: api.JSONBase{ - Kind: "Pod", + Kind: "Pod", + APIVersion: "v1beta1", }, Labels: controllerSpec.DesiredState.PodTemplate.Labels, DesiredState: controllerSpec.DesiredState.PodTemplate.DesiredState, @@ -207,12 +211,12 @@ func TestCreateReplica(t *testing.T) { } if !reflect.DeepEqual(expectedPod, actualPod) { t.Logf("Body: %s", fakeHandler.RequestBody) - t.Errorf("Unexpected mismatch. Expected %#v, Got: %#v", expectedPod, actualPod) + t.Errorf("Unexpected mismatch. Expected\n %#v,\n Got:\n %#v", expectedPod, actualPod) } } func TestHandleWatchResponseNotSet(t *testing.T) { - body, _ := json.Marshal(makePodList(2)) + body, _ := api.Encode(makePodList(2)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -233,7 +237,7 @@ func TestHandleWatchResponseNotSet(t *testing.T) { } func TestHandleWatchResponseNoNode(t *testing.T) { - body, _ := json.Marshal(makePodList(2)) + body, _ := api.Encode(makePodList(2)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -254,7 +258,7 @@ func TestHandleWatchResponseNoNode(t *testing.T) { } func TestHandleWatchResponseBadData(t *testing.T) { - body, _ := json.Marshal(makePodList(2)) + body, _ := api.Encode(makePodList(2)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -278,7 +282,7 @@ func TestHandleWatchResponseBadData(t *testing.T) { } func TestHandleWatchResponse(t *testing.T) { - body, _ := json.Marshal(makePodList(2)) + body, _ := api.Encode(makePodList(2)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -293,6 +297,7 @@ func TestHandleWatchResponse(t *testing.T) { controller := makeReplicationController(2) + // TODO: fixme when etcd uses Encode/Decode data, err := json.Marshal(controller) if err != nil { t.Errorf("Unexpected error: %v", err) @@ -312,7 +317,7 @@ func TestHandleWatchResponse(t *testing.T) { } func TestHandleWatchResponseDelete(t *testing.T) { - body, _ := json.Marshal(makePodList(2)) + body, _ := api.Encode(makePodList(2)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), @@ -327,6 +332,7 @@ func TestHandleWatchResponseDelete(t *testing.T) { controller := makeReplicationController(2) + // TODO: fixme when etcd writing uses api.Encode data, err := json.Marshal(controller) if err != nil { t.Errorf("Unexpected error: %v", err) @@ -347,6 +353,7 @@ func TestHandleWatchResponseDelete(t *testing.T) { func TestSyncronize(t *testing.T) { controllerSpec1 := api.ReplicationController{ + JSONBase: api.JSONBase{APIVersion: "v1beta1"}, DesiredState: api.ReplicationControllerState{ Replicas: 4, PodTemplate: api.PodTemplate{ @@ -367,6 +374,7 @@ func TestSyncronize(t *testing.T) { }, } controllerSpec2 := api.ReplicationController{ + JSONBase: api.JSONBase{APIVersion: "v1beta1"}, DesiredState: api.ReplicationControllerState{ Replicas: 3, PodTemplate: api.PodTemplate{ @@ -405,7 +413,7 @@ func TestSyncronize(t *testing.T) { fakeHandler := util.FakeHandler{ StatusCode: 200, - ResponseBody: "{}", + ResponseBody: "{\"apiVersion\": \"v1beta1\", \"kind\": \"PodList\"}", T: t, } testServer := httptest.NewTLSServer(&fakeHandler) diff --git a/pkg/kubecfg/parse_test.go b/pkg/kubecfg/parse_test.go index 113c70d092bbf..1fd36ad9e1e02 100644 --- a/pkg/kubecfg/parse_test.go +++ b/pkg/kubecfg/parse_test.go @@ -34,7 +34,7 @@ func DoParseTest(t *testing.T, storage string, obj interface{}) { jsonData, _ := api.Encode(obj) yamlData, _ := yaml.Marshal(obj) t.Logf("Intermediate yaml:\n%v\n", string(yamlData)) - + t.Logf("Intermediate json:\n%v\n", string(jsonData)) jsonGot, jsonErr := ToWireFormat(jsonData, storage) yamlGot, yamlErr := ToWireFormat(yamlData, storage) @@ -56,7 +56,7 @@ func DoParseTest(t *testing.T, storage string, obj interface{}) { func TestParsePod(t *testing.T) { DoParseTest(t, "pods", api.Pod{ - JSONBase: api.JSONBase{ID: "test pod"}, + JSONBase: api.JSONBase{APIVersion: "v1beta1", ID: "test pod", Kind: "Pod"}, DesiredState: api.PodState{ Manifest: api.ContainerManifest{ ID: "My manifest", @@ -73,7 +73,7 @@ func TestParsePod(t *testing.T) { func TestParseService(t *testing.T) { DoParseTest(t, "services", api.Service{ - JSONBase: api.JSONBase{ID: "my service"}, + JSONBase: api.JSONBase{APIVersion: "v1beta1", ID: "my service", Kind: "Service"}, Port: 8080, Labels: map[string]string{ "area": "staging", @@ -86,6 +86,7 @@ func TestParseService(t *testing.T) { func TestParseController(t *testing.T) { DoParseTest(t, "replicationControllers", api.ReplicationController{ + JSONBase: api.JSONBase{APIVersion: "v1beta1", ID: "my controller", Kind: "ReplicationController"}, DesiredState: api.ReplicationControllerState{ Replicas: 9001, PodTemplate: api.PodTemplate{ diff --git a/pkg/registry/endpoints_test.go b/pkg/registry/endpoints_test.go index 6a0b696b12eb3..e8ed5b93ddc8e 100644 --- a/pkg/registry/endpoints_test.go +++ b/pkg/registry/endpoints_test.go @@ -32,7 +32,8 @@ func makePodList(count int) api.PodList { for i := 0; i < count; i++ { pods = append(pods, api.Pod{ JSONBase: api.JSONBase{ - ID: fmt.Sprintf("pod%d", i), + ID: fmt.Sprintf("pod%d", i), + APIVersion: "v1beta1", }, DesiredState: api.PodState{ Manifest: api.ContainerManifest{ @@ -53,7 +54,8 @@ func makePodList(count int) api.PodList { }) } return api.PodList{ - Items: pods, + JSONBase: api.JSONBase{APIVersion: "v1beta1", Kind: "PodList"}, + Items: pods, } } diff --git a/pkg/tools/decoder_test.go b/pkg/tools/decoder_test.go index 9d500bb321459..6fabc5646b76a 100644 --- a/pkg/tools/decoder_test.go +++ b/pkg/tools/decoder_test.go @@ -21,6 +21,7 @@ import ( "io" "reflect" "testing" + "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -39,18 +40,28 @@ func TestDecoder(t *testing.T) { } }() - action, got, err := decoder.Decode() - if err != nil { - t.Errorf("Unexpected error %v", err) - } - if e, a := watch.Added, action; e != a { - t.Errorf("Expected %v, got %v", e, a) - } - if e, a := expect, got; !reflect.DeepEqual(e, a) { - t.Errorf("Expected %v, got %v", e, a) + done := make(chan struct{}) + go func() { + action, got, err := decoder.Decode() + if err != nil { + t.Errorf("Unexpected error %v", err) + } + if e, a := watch.Added, action; e != a { + t.Errorf("Expected %v, got %v", e, a) + } + if e, a := expect, got; !reflect.DeepEqual(e, a) { + t.Errorf("Expected %v, got %v", e, a) + } + close(done) + }() + select { + case <-done: + break + case <-time.After(10 * time.Second): + t.Error("Timeout") } - done := make(chan struct{}) + done = make(chan struct{}) go func() { _, _, err := decoder.Decode() @@ -62,7 +73,12 @@ func TestDecoder(t *testing.T) { decoder.Close() - <-done + select { + case <-done: + break + case <-time.After(10 * time.Second): + t.Error("Timeout") + } } func TestDecoder_SourceClose(t *testing.T) { @@ -81,5 +97,10 @@ func TestDecoder_SourceClose(t *testing.T) { in.Close() - <-done + select { + case <-done: + break + case <-time.After(10 * time.Second): + t.Error("Timeout") + } } From 3723eb76293edb013d48eb9061c7e724ca41bb90 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 23 Jul 2014 22:32:07 -0700 Subject: [PATCH 2/2] Add kind and apiVersion to guestbook example. --- README.md | 2 ++ api/examples/controller-list.json | 4 +++- api/examples/controller.json | 1 + api/examples/external-service.json | 2 ++ api/examples/pod-list.json | 2 ++ api/examples/pod.json | 2 ++ api/examples/service-list.json | 2 ++ api/examples/service.json | 2 ++ examples/guestbook/frontend-controller.json | 2 ++ examples/guestbook/frontend-service.json | 2 ++ examples/guestbook/redis-master-service.json | 2 ++ examples/guestbook/redis-master.json | 2 ++ examples/guestbook/redis-slave-controller.json | 2 ++ examples/guestbook/redis-slave-service.json | 2 ++ 14 files changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 20569d435f388..521f447f70457 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ Where pod.json contains something like: ``` { "id": "php", + "kind": "Pod", + "apiVersion": "v1beta1", "desiredState": { "manifest": { "version": "v1beta1", diff --git a/api/examples/controller-list.json b/api/examples/controller-list.json index c3c1d969d2c8c..4cadf17686582 100644 --- a/api/examples/controller-list.json +++ b/api/examples/controller-list.json @@ -1,4 +1,6 @@ { + "kind": "ReplicationControllerList", + "apiVersion": "v1beta1", "items": [ { "id": "testRun", @@ -30,4 +32,4 @@ } } ] -} \ No newline at end of file +} diff --git a/api/examples/controller.json b/api/examples/controller.json index ea04e69d28319..d385569898b85 100644 --- a/api/examples/controller.json +++ b/api/examples/controller.json @@ -1,6 +1,7 @@ { "id": "nginxController", "apiVersion": "v1beta1", + "kind": "ReplicationController", "desiredState": { "replicas": 2, "replicaSelector": {"name": "nginx"}, diff --git a/api/examples/external-service.json b/api/examples/external-service.json index e841b2e946b38..95d796e5049dc 100644 --- a/api/examples/external-service.json +++ b/api/examples/external-service.json @@ -1,5 +1,7 @@ { "id": "example", + "kind": "Service", + "apiVersion": "v1beta1", "port": 8000, "labels": { "name": "nginx" diff --git a/api/examples/pod-list.json b/api/examples/pod-list.json index e9259424929f8..ea6f9f874e9bf 100644 --- a/api/examples/pod-list.json +++ b/api/examples/pod-list.json @@ -1,4 +1,6 @@ { + "kind": "PodList", + "apiVersion": "v1beta1", "items": [ { "id": "my-pod-1", diff --git a/api/examples/pod.json b/api/examples/pod.json index 5ab5bc461ae25..f3157ad3f3100 100644 --- a/api/examples/pod.json +++ b/api/examples/pod.json @@ -1,4 +1,6 @@ { + "kind": "Pod", + "apiVersion": "v1beta1", "id": "php", "desiredState": { "manifest": { diff --git a/api/examples/service-list.json b/api/examples/service-list.json index 87aff59381714..1ac2963b1643c 100644 --- a/api/examples/service-list.json +++ b/api/examples/service-list.json @@ -1,4 +1,6 @@ { + "kind": "ServiceList", + "apiVersion": "v1beta1", "items": [ { "id": "example1", diff --git a/api/examples/service.json b/api/examples/service.json index f9616e62813fa..f4ae8993db3a0 100644 --- a/api/examples/service.json +++ b/api/examples/service.json @@ -1,4 +1,6 @@ { + "kind": "Service", + "apiVersion": "v1beta1", "id": "example", "port": 8000, "labels": { diff --git a/examples/guestbook/frontend-controller.json b/examples/guestbook/frontend-controller.json index 9302f560a432f..c98c26b0731a6 100644 --- a/examples/guestbook/frontend-controller.json +++ b/examples/guestbook/frontend-controller.json @@ -1,5 +1,7 @@ { "id": "frontendController", + "kind": "ReplicationController", + "apiVersion": "v1beta1", "desiredState": { "replicas": 3, "replicaSelector": {"name": "frontend"}, diff --git a/examples/guestbook/frontend-service.json b/examples/guestbook/frontend-service.json index 11d4bba177780..3c49cc59d35d8 100644 --- a/examples/guestbook/frontend-service.json +++ b/examples/guestbook/frontend-service.json @@ -1,5 +1,7 @@ { "id": "frontend", + "kind": "Service", + "apiVersion": "v1beta1", "port": 9998, "selector": { "name": "frontend" diff --git a/examples/guestbook/redis-master-service.json b/examples/guestbook/redis-master-service.json index 5d79ea37fdd3e..a66dd9be7bc72 100644 --- a/examples/guestbook/redis-master-service.json +++ b/examples/guestbook/redis-master-service.json @@ -1,5 +1,7 @@ { "id": "redismaster", + "kind": "Service", + "apiVersion": "v1beta1", "port": 10000, "selector": { "name": "redis-master" diff --git a/examples/guestbook/redis-master.json b/examples/guestbook/redis-master.json index 355f44134589d..313f9c4e06c96 100644 --- a/examples/guestbook/redis-master.json +++ b/examples/guestbook/redis-master.json @@ -1,5 +1,7 @@ { "id": "redis-master-2", + "kind": "Pod", + "apiVersion": "v1beta1", "desiredState": { "manifest": { "version": "v1beta1", diff --git a/examples/guestbook/redis-slave-controller.json b/examples/guestbook/redis-slave-controller.json index cfee708a99a69..1f0db5f06f554 100644 --- a/examples/guestbook/redis-slave-controller.json +++ b/examples/guestbook/redis-slave-controller.json @@ -1,5 +1,7 @@ { "id": "redisSlaveController", + "kind": "ReplicationController", + "apiVersion": "v1beta1", "desiredState": { "replicas": 2, "replicaSelector": {"name": "redisslave"}, diff --git a/examples/guestbook/redis-slave-service.json b/examples/guestbook/redis-slave-service.json index b317ae856e623..5525b6314cea4 100644 --- a/examples/guestbook/redis-slave-service.json +++ b/examples/guestbook/redis-slave-service.json @@ -1,5 +1,7 @@ { "id": "redisslave", + "kind": "Service", + "apiVersion": "v1beta1", "port": 10001, "labels": { "name": "redisslave"