diff --git a/cmd/kube-apiserver/apiserver.go b/cmd/kube-apiserver/apiserver.go index cdcc5d997d9..1ee5c938af7 100644 --- a/cmd/kube-apiserver/apiserver.go +++ b/cmd/kube-apiserver/apiserver.go @@ -82,6 +82,7 @@ var ( allowPrivileged = flag.Bool("allow_privileged", false, "If true, allow privileged containers.") portalNet util.IPNet // TODO: make this a list enableLogsSupport = flag.Bool("enable_logs_support", true, "Enables server endpoint for log collection") + runtimeConfig util.ConfigurationMap kubeletConfig = client.KubeletConfig{ Port: 10250, EnableHttps: false, @@ -89,10 +90,13 @@ var ( ) func init() { + runtimeConfig = make(util.ConfigurationMap) + flag.Var(&address, "address", "The IP address on to serve on (set to 0.0.0.0 for all interfaces)") flag.Var(&etcdServerList, "etcd_servers", "List of etcd servers to watch (http://ip:port), comma separated. Mutually exclusive with -etcd_config") flag.Var(&corsAllowedOriginList, "cors_allowed_origins", "List of allowed origins for CORS, comma separated. An allowed origin can be a regular expression to support subdomain matching. If this list is empty CORS will not be enabled.") flag.Var(&portalNet, "portal_net", "A CIDR notation IP range from which to assign portal IPs. This must not overlap with any IP ranges assigned to nodes for pods.") + flag.Var(&runtimeConfig, "runtime_config", "A set of key=value pairs that describe runtime configuration that may be passed to the apiserver.") client.BindKubeletClientConfigFlags(flag.CommandLine, &kubeletConfig) } @@ -140,6 +144,8 @@ func main() { glog.Fatalf("Failure to start kubelet client: %v", err) } + _, v1beta3 := runtimeConfig["api/v1beta3"] + // TODO: expose same flags as client.BindClientConfigFlags but for a server clientConfig := &client.Config{ Host: net.JoinHostPort(address.String(), strconv.Itoa(int(*port))), @@ -189,6 +195,7 @@ func main() { Authenticator: authenticator, Authorizer: authorizer, AdmissionControl: admissionController, + EnableV1Beta3: v1beta3, } m := master.New(config) diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index 4fc990c860b..4408ace88e8 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -92,6 +92,7 @@ sudo "${GO_OUT}/kube-apiserver" \ -v=${LOG_LEVEL} \ --address="${API_HOST}" \ --port="${API_PORT}" \ + --runtime_config=api/v1beta3 \ --etcd_servers="http://127.0.0.1:4001" \ --portal_net="10.0.0.0/24" \ --cors_allowed_origins="${API_CORS_ALLOWED_ORIGINS}" >"${APISERVER_LOG}" 2>&1 & diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 29f4291c492..cd244bcddc1 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -74,6 +74,7 @@ kube::log::status "Starting kube-apiserver" --etcd_servers="http://${ETCD_HOST}:${ETCD_PORT}" \ --public_address_override="127.0.0.1" \ --kubelet_port=${KUBELET_PORT} \ + --runtime_config=api/v1beta3 \ --portal_net="10.0.0.0/24" 1>&2 & APISERVER_PID=$! @@ -87,6 +88,7 @@ kube::log::status "Starting CONTROLLER-MANAGER" CTLRMGR_PID=$! kube::util::wait_for_url "http://127.0.0.1:${CTLRMGR_PORT}/healthz" "controller-manager: " +kube::util::wait_for_url "http://127.0.0.1:${API_PORT}/api/v1beta1/minions/127.0.0.1" "apiserver(minions): " 0.2 25 kube_cmd=( "${KUBE_OUTPUT_HOSTBIN}/kubectl" @@ -94,6 +96,7 @@ kube_cmd=( kube_api_versions=( v1beta1 v1beta2 + v1beta3 ) for version in "${kube_api_versions[@]}"; do kube_flags=( @@ -102,7 +105,7 @@ for version in "${kube_api_versions[@]}"; do --api-version="${version}" ) - kube::log::status "Testing kubectl(pods)" + kube::log::status "Testing kubectl(${version}:pods)" "${kube_cmd[@]}" get pods "${kube_flags[@]}" "${kube_cmd[@]}" create -f examples/guestbook/redis-master.json "${kube_flags[@]}" "${kube_cmd[@]}" get pods "${kube_flags[@]}" @@ -110,34 +113,36 @@ for version in "${kube_api_versions[@]}"; do [[ "$("${kube_cmd[@]}" get pod redis-master -o template --output-version=v1beta1 -t '{{ .id }}' "${kube_flags[@]}")" == "redis-master" ]] output_pod=$("${kube_cmd[@]}" get pod redis-master -o json --output-version=v1beta1 "${kube_flags[@]}") "${kube_cmd[@]}" delete pod redis-master "${kube_flags[@]}" - before="$("${kube_cmd[@]}" get pods -o template -t '{{ len .items }}' "${kube_flags[@]}")" + before="$("${kube_cmd[@]}" get pods -o template -t "{{ len .items }}" "${kube_flags[@]}")" echo $output_pod | "${kube_cmd[@]}" create -f - "${kube_flags[@]}" - after="$("${kube_cmd[@]}" get pods -o template -t '{{ len .items }}' "${kube_flags[@]}")" + after="$("${kube_cmd[@]}" get pods -o template -t "{{ len .items }}" "${kube_flags[@]}")" [[ "$((${after} - ${before}))" -eq 1 ]] - "${kube_cmd[@]}" get pods -o yaml "${kube_flags[@]}" | grep -q "id: redis-master" + "${kube_cmd[@]}" get pods -o yaml --output-version=v1beta1 "${kube_flags[@]}" | grep -q "id: redis-master" "${kube_cmd[@]}" describe pod redis-master "${kube_flags[@]}" | grep -q 'Name:.*redis-master' "${kube_cmd[@]}" delete -f examples/guestbook/redis-master.json "${kube_flags[@]}" - kube::log::status "Testing kubectl(services)" + kube::log::status "Testing kubectl(${version}:services)" "${kube_cmd[@]}" get services "${kube_flags[@]}" "${kube_cmd[@]}" create -f examples/guestbook/frontend-service.json "${kube_flags[@]}" "${kube_cmd[@]}" get services "${kube_flags[@]}" "${kube_cmd[@]}" delete service frontend "${kube_flags[@]}" - kube::log::status "Testing kubectl(replicationcontrollers)" + kube::log::status "Testing kubectl(${version}:replicationcontrollers)" "${kube_cmd[@]}" get replicationcontrollers "${kube_flags[@]}" "${kube_cmd[@]}" create -f examples/guestbook/frontend-controller.json "${kube_flags[@]}" "${kube_cmd[@]}" get replicationcontrollers "${kube_flags[@]}" "${kube_cmd[@]}" describe replicationcontroller frontendController "${kube_flags[@]}" | grep -q 'Replicas:.*3 desired' "${kube_cmd[@]}" delete rc frontendController "${kube_flags[@]}" - kube::log::status "Testing kubectl(minions)" - "${kube_cmd[@]}" get minions "${kube_flags[@]}" - "${kube_cmd[@]}" get minions 127.0.0.1 "${kube_flags[@]}" - - kube::log::status "Testing kubectl(nodes)" + kube::log::status "Testing kubectl(${version}:nodes)" "${kube_cmd[@]}" get nodes "${kube_flags[@]}" "${kube_cmd[@]}" describe nodes 127.0.0.1 "${kube_flags[@]}" + + if [[ "${version}" != "v1beta3" ]]; then + kube::log::status "Testing kubectl(${version}:minions)" + "${kube_cmd[@]}" get minions "${kube_flags[@]}" + "${kube_cmd[@]}" get minions 127.0.0.1 "${kube_flags[@]}" + fi done kube::log::status "TEST PASSED" diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index afed8ce60c1..2a13f1a3561 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -24,6 +24,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) @@ -38,7 +39,7 @@ const OldestVersion = "v1beta1" // may be assumed to be least feature rich to most feature rich, and clients may // choose to prefer the latter items in the list over the former items when presented // with a set of versions to choose. -var Versions = []string{"v1beta1", "v1beta2"} +var Versions = []string{"v1beta1", "v1beta2", "v1beta3"} // Codec is the default codec for serializing output that should use // the latest supported version. Use this Codec when writing to @@ -80,6 +81,12 @@ func InterfacesFor(version string) (*meta.VersionInterfaces, error) { ObjectConvertor: api.Scheme, MetadataAccessor: accessor, }, nil + case "v1beta3": + return &meta.VersionInterfaces{ + Codec: v1beta3.Codec, + ObjectConvertor: api.Scheme, + MetadataAccessor: accessor, + }, nil default: return nil, fmt.Errorf("unsupported storage version: %s (valid: %s)", version, strings.Join(Versions, ", ")) } @@ -96,7 +103,7 @@ func init() { return interfaces, true }, ) - mapper.Add(api.Scheme, true, Versions...) - // TODO: when v1beta3 is added it will not use mixed case. + mapper.Add(api.Scheme, true, "v1beta1", "v1beta2") + mapper.Add(api.Scheme, false, "v1beta3") RESTMapper = mapper } diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 46330c4e10d..c05d13b5ffe 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -18,6 +18,7 @@ package api_test import ( "encoding/json" + "flag" "math/rand" "reflect" @@ -30,6 +31,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -38,188 +40,220 @@ import ( "speter.net/go/exp/math/dec/inf" ) -var fuzzIters = flag.Int("fuzz_iters", 40, "How many fuzzing iterations to do.") +var fuzzIters = flag.Int("fuzz_iters", 20, "How many fuzzing iterations to do.") -// apiObjectFuzzer can randomly populate api objects. -var apiObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 1).Funcs( - func(j *runtime.PluginBase, c fuzz.Continue) { - // Do nothing; this struct has only a Kind field and it must stay blank in memory. - }, - func(j *runtime.TypeMeta, c fuzz.Continue) { - // We have to customize the randomization of TypeMetas because their - // APIVersion and Kind must remain blank in memory. - j.APIVersion = "" - j.Kind = "" - }, - func(j *api.TypeMeta, c fuzz.Continue) { - // We have to customize the randomization of TypeMetas because their - // APIVersion and Kind must remain blank in memory. - j.APIVersion = "" - j.Kind = "" - }, - func(j *api.ObjectMeta, c fuzz.Continue) { - j.Name = c.RandString() - j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) - j.SelfLink = c.RandString() +// fuzzerFor can randomly populate api objects that are destined for version. +func fuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { + f := fuzz.New().NilChance(.5).NumElements(1, 1) + if src != nil { + f.RandSource(src) + } + f.Funcs( + func(j *runtime.PluginBase, c fuzz.Continue) { + // Do nothing; this struct has only a Kind field and it must stay blank in memory. + }, + func(j *runtime.TypeMeta, c fuzz.Continue) { + // We have to customize the randomization of TypeMetas because their + // APIVersion and Kind must remain blank in memory. + j.APIVersion = "" + j.Kind = "" + }, + func(j *api.TypeMeta, c fuzz.Continue) { + // We have to customize the randomization of TypeMetas because their + // APIVersion and Kind must remain blank in memory. + j.APIVersion = "" + j.Kind = "" + }, + func(j *api.ObjectMeta, c fuzz.Continue) { + j.Name = c.RandString() + j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) + j.SelfLink = c.RandString() - var sec, nsec int64 - c.Fuzz(&sec) - c.Fuzz(&nsec) - j.CreationTimestamp = util.Unix(sec, nsec).Rfc3339Copy() - }, - func(j *api.ListMeta, c fuzz.Continue) { - j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) - j.SelfLink = c.RandString() - }, - func(j *api.PodPhase, c fuzz.Continue) { - statuses := []api.PodPhase{api.PodPending, api.PodRunning, api.PodFailed, api.PodUnknown} - *j = statuses[c.Rand.Intn(len(statuses))] - }, - func(j *api.ReplicationControllerSpec, c fuzz.Continue) { - // TemplateRef must be nil for round trip - c.Fuzz(&j.Template) - if j.Template == nil { - // TODO: v1beta1/2 can't round trip a nil template correctly, fix by having v1beta1/2 - // conversion compare converted object to nil via DeepEqual - j.Template = &api.PodTemplateSpec{} - } - j.Template.ObjectMeta = api.ObjectMeta{Labels: j.Template.ObjectMeta.Labels} - j.Template.Spec.NodeSelector = nil - c.Fuzz(&j.Selector) - j.Replicas = int(c.RandUint64()) - }, - func(j *api.ReplicationControllerStatus, c fuzz.Continue) { - // only replicas round trips - j.Replicas = int(c.RandUint64()) - }, - func(j *api.List, c fuzz.Continue) { - c.Fuzz(&j.ListMeta) - c.Fuzz(&j.Items) - if j.Items == nil { - j.Items = []runtime.Object{} - } - }, - func(j *runtime.Object, c fuzz.Continue) { - if c.RandBool() { - *j = &runtime.Unknown{ - TypeMeta: runtime.TypeMeta{Kind: "Something", APIVersion: "unknown"}, - RawJSON: []byte(`{"apiVersion":"unknown","kind":"Something","someKey":"someValue"}`), + var sec, nsec int64 + c.Fuzz(&sec) + c.Fuzz(&nsec) + j.CreationTimestamp = util.Unix(sec, nsec).Rfc3339Copy() + }, + func(j *api.ListMeta, c fuzz.Continue) { + j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10) + j.SelfLink = c.RandString() + }, + func(j *api.PodPhase, c fuzz.Continue) { + statuses := []api.PodPhase{api.PodPending, api.PodRunning, api.PodFailed, api.PodUnknown} + *j = statuses[c.Rand.Intn(len(statuses))] + }, + func(j *api.ReplicationControllerSpec, c fuzz.Continue) { + // TemplateRef must be nil for round trip + c.Fuzz(&j.Template) + if j.Template == nil { + // TODO: v1beta1/2 can't round trip a nil template correctly, fix by having v1beta1/2 + // conversion compare converted object to nil via DeepEqual + j.Template = &api.PodTemplateSpec{} } - } else { - types := []runtime.Object{&api.Pod{}, &api.ReplicationController{}} - t := types[c.Rand.Intn(len(types))] - c.Fuzz(t) - *j = t - } - }, - func(intstr *util.IntOrString, c fuzz.Continue) { - // util.IntOrString will panic if its kind is set wrong. - if c.RandBool() { - intstr.Kind = util.IntstrInt - intstr.IntVal = int(c.RandUint64()) - intstr.StrVal = "" - } else { - intstr.Kind = util.IntstrString - intstr.IntVal = 0 - intstr.StrVal = c.RandString() - } - }, - func(pb map[docker.Port][]docker.PortBinding, c fuzz.Continue) { - // This is necessary because keys with nil values get omitted. - // TODO: Is this a bug? - pb[docker.Port(c.RandString())] = []docker.PortBinding{ - {c.RandString(), c.RandString()}, - {c.RandString(), c.RandString()}, - } - }, - func(pm map[string]docker.PortMapping, c fuzz.Continue) { - // This is necessary because keys with nil values get omitted. - // TODO: Is this a bug? - pm[c.RandString()] = docker.PortMapping{ - c.RandString(): c.RandString(), - } - }, + j.Template.ObjectMeta = api.ObjectMeta{Labels: j.Template.ObjectMeta.Labels} + j.Template.Spec.NodeSelector = nil + c.Fuzz(&j.Selector) + j.Replicas = int(c.RandUint64()) + }, + func(j *api.ReplicationControllerStatus, c fuzz.Continue) { + // only replicas round trips + j.Replicas = int(c.RandUint64()) + }, + func(j *api.List, c fuzz.Continue) { + c.Fuzz(&j.ListMeta) + c.Fuzz(&j.Items) + if j.Items == nil { + j.Items = []runtime.Object{} + } + }, + func(j *runtime.Object, c fuzz.Continue) { + if c.RandBool() { + *j = &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{Kind: "Something", APIVersion: "unknown"}, + RawJSON: []byte(`{"apiVersion":"unknown","kind":"Something","someKey":"someValue"}`), + } + } else { + types := []runtime.Object{&api.Pod{}, &api.ReplicationController{}} + t := types[c.Rand.Intn(len(types))] + c.Fuzz(t) + *j = t + } + }, + func(intstr *util.IntOrString, c fuzz.Continue) { + // util.IntOrString will panic if its kind is set wrong. + if c.RandBool() { + intstr.Kind = util.IntstrInt + intstr.IntVal = int(c.RandUint64()) + intstr.StrVal = "" + } else { + intstr.Kind = util.IntstrString + intstr.IntVal = 0 + intstr.StrVal = c.RandString() + } + }, + func(pb map[docker.Port][]docker.PortBinding, c fuzz.Continue) { + // This is necessary because keys with nil values get omitted. + // TODO: Is this a bug? + pb[docker.Port(c.RandString())] = []docker.PortBinding{ + {c.RandString(), c.RandString()}, + {c.RandString(), c.RandString()}, + } + }, + func(pm map[string]docker.PortMapping, c fuzz.Continue) { + // This is necessary because keys with nil values get omitted. + // TODO: Is this a bug? + pm[c.RandString()] = docker.PortMapping{ + c.RandString(): c.RandString(), + } + }, - func(q *resource.Quantity, c fuzz.Continue) { - // Real Quantity fuzz testing is done elsewhere; - // this limited subset of functionality survives - // round-tripping to v1beta1/2. - q.Amount = &inf.Dec{} - q.Format = resource.DecimalExponent - //q.Amount.SetScale(inf.Scale(-c.Intn(12))) - q.Amount.SetUnscaled(c.Int63n(1000)) - }, -) + func(q *resource.Quantity, c fuzz.Continue) { + // Real Quantity fuzz testing is done elsewhere; + // this limited subset of functionality survives + // round-tripping to v1beta1/2. + q.Amount = &inf.Dec{} + q.Format = resource.DecimalExponent + //q.Amount.SetScale(inf.Scale(-c.Intn(12))) + q.Amount.SetUnscaled(c.Int63n(1000)) + }, + ) + return f +} -func runTest(t *testing.T, codec runtime.Codec, source runtime.Object) { - name := reflect.TypeOf(source).Elem().Name() - apiObjectFuzzer.Fuzz(source) - j, err := meta.Accessor(source) +func fuzzInternalObject(t *testing.T, forVersion string, item runtime.Object, seed int64) runtime.Object { + fuzzerFor(t, forVersion, rand.NewSource(seed)).Fuzz(item) + + j, err := meta.Accessor(item) if err != nil { - t.Fatalf("Unexpected error %v for %#v", err, source) + t.Fatalf("Unexpected error %v for %#v", err, item) } j.SetKind("") j.SetAPIVersion("") - data, err := codec.Encode(source) + return item +} + +func roundTrip(t *testing.T, codec runtime.Codec, item runtime.Object) { + name := reflect.TypeOf(item).Elem().Name() + data, err := codec.Encode(item) if err != nil { - t.Errorf("%v: %v (%#v)", name, err, source) + t.Errorf("%v: %v (%#v)", name, err, item) return } obj2, err := codec.Decode(data) if err != nil { - t.Errorf("0: %v: %v\nCodec: %v\nData: %s\nSource: %#v", name, err, codec, string(data), source) + t.Errorf("0: %v: %v\nCodec: %v\nData: %s\nSource: %#v", name, err, codec, string(data), item) return } - if !api.Semantic.DeepEqual(source, obj2) { - t.Errorf("1: %v: diff: %v\nCodec: %v\nData: %s\nSource: %#v", name, util.ObjectGoPrintDiff(source, obj2), codec, string(data), source) + if !api.Semantic.DeepEqual(item, obj2) { + t.Errorf("1: %v: diff: %v\nCodec: %v\nData: %s\nSource: %#v\nFinal: %#v", name, util.ObjectGoPrintDiff(item, obj2), codec, string(data), item, obj2) return } - obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface().(runtime.Object) + obj3 := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object) err = codec.DecodeInto(data, obj3) if err != nil { t.Errorf("2: %v: %v", name, err) return } - if !api.Semantic.DeepEqual(source, obj3) { - t.Errorf("3: %v: diff: %v\nCodec: %v", name, util.ObjectDiff(source, obj3), codec) + if !api.Semantic.DeepEqual(item, obj3) { + t.Errorf("3: %v: diff: %v\nCodec: %v", name, util.ObjectDiff(item, obj3), codec) return } } +// roundTripSame verifies the same source object is tested in all API versions. +func roundTripSame(t *testing.T, item runtime.Object) { + seed := rand.Int63() + fuzzInternalObject(t, "", item, seed) + roundTrip(t, v1beta1.Codec, item) + roundTrip(t, v1beta2.Codec, item) + fuzzInternalObject(t, "v1beta3", item, seed) + roundTrip(t, v1beta3.Codec, item) +} + +func roundTripAll(t *testing.T, item runtime.Object) { + seed := rand.Int63() + roundTrip(t, v1beta1.Codec, fuzzInternalObject(t, "v1beta1", item, seed)) + roundTrip(t, v1beta2.Codec, fuzzInternalObject(t, "v1beta2", item, seed)) + roundTrip(t, v1beta3.Codec, fuzzInternalObject(t, "v1beta3", item, seed)) +} + // For debugging problems func TestSpecificKind(t *testing.T) { api.Scheme.Log(t) + defer api.Scheme.Log(nil) + kind := "PodList" item, err := api.Scheme.New("", kind) if err != nil { t.Errorf("Couldn't make a %v? %v", kind, err) return } - runTest(t, v1beta1.Codec, item) - runTest(t, v1beta2.Codec, item) - api.Scheme.Log(nil) + roundTripSame(t, item) } func TestList(t *testing.T) { api.Scheme.Log(t) + defer api.Scheme.Log(nil) + kind := "List" item, err := api.Scheme.New("", kind) if err != nil { t.Errorf("Couldn't make a %v? %v", kind, err) return } - runTest(t, v1beta1.Codec, item) - runTest(t, v1beta2.Codec, item) - api.Scheme.Log(nil) + roundTripSame(t, item) } -var nonRoundTrippableTypes = util.NewStringSet("ContainerManifest") +var nonRoundTrippableTypes = util.NewStringSet("ContainerManifest", "ContainerManifestList") var nonInternalRoundTrippableTypes = util.NewStringSet("List") func TestRoundTripTypes(t *testing.T) { + // api.Scheme.Log(t) + // defer api.Scheme.Log(nil) + for kind := range api.Scheme.KnownTypes("") { if nonRoundTrippableTypes.Has(kind) { continue @@ -233,10 +267,9 @@ func TestRoundTripTypes(t *testing.T) { if _, err := meta.Accessor(item); err != nil { t.Fatalf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableTypes: %v", kind, err) } - runTest(t, v1beta1.Codec, item) - runTest(t, v1beta2.Codec, item) + roundTripSame(t, item) if !nonInternalRoundTrippableTypes.Has(kind) { - runTest(t, api.Codec, item) + roundTrip(t, api.Codec, fuzzInternalObject(t, "", item, rand.Int63())) } } } @@ -281,7 +314,7 @@ const benchmarkSeed = 100 func BenchmarkEncode(b *testing.B) { pod := api.Pod{} - apiObjectFuzzer.RandSource(rand.NewSource(benchmarkSeed)) + apiObjectFuzzer := fuzzerFor(nil, "", rand.NewSource(benchmarkSeed)) apiObjectFuzzer.Fuzz(&pod) for i := 0; i < b.N; i++ { latest.Codec.Encode(&pod) @@ -291,7 +324,7 @@ func BenchmarkEncode(b *testing.B) { // BenchmarkEncodeJSON provides a baseline for regular JSON encode performance func BenchmarkEncodeJSON(b *testing.B) { pod := api.Pod{} - apiObjectFuzzer.RandSource(rand.NewSource(benchmarkSeed)) + apiObjectFuzzer := fuzzerFor(nil, "", rand.NewSource(benchmarkSeed)) apiObjectFuzzer.Fuzz(&pod) for i := 0; i < b.N; i++ { json.Marshal(&pod) @@ -300,7 +333,7 @@ func BenchmarkEncodeJSON(b *testing.B) { func BenchmarkDecode(b *testing.B) { pod := api.Pod{} - apiObjectFuzzer.RandSource(rand.NewSource(benchmarkSeed)) + apiObjectFuzzer := fuzzerFor(nil, "", rand.NewSource(benchmarkSeed)) apiObjectFuzzer.Fuzz(&pod) data, _ := latest.Codec.Encode(&pod) for i := 0; i < b.N; i++ { @@ -310,7 +343,7 @@ func BenchmarkDecode(b *testing.B) { func BenchmarkDecodeInto(b *testing.B) { pod := api.Pod{} - apiObjectFuzzer.RandSource(rand.NewSource(benchmarkSeed)) + apiObjectFuzzer := fuzzerFor(nil, "", rand.NewSource(benchmarkSeed)) apiObjectFuzzer.Fuzz(&pod) data, _ := latest.Codec.Encode(&pod) for i := 0; i < b.N; i++ { @@ -322,7 +355,7 @@ func BenchmarkDecodeInto(b *testing.B) { // BenchmarkDecodeJSON provides a baseline for regular JSON decode performance func BenchmarkDecodeJSON(b *testing.B) { pod := api.Pod{} - apiObjectFuzzer.RandSource(rand.NewSource(benchmarkSeed)) + apiObjectFuzzer := fuzzerFor(nil, "", rand.NewSource(benchmarkSeed)) apiObjectFuzzer.Fuzz(&pod) data, _ := latest.Codec.Encode(&pod) for i := 0; i < b.N; i++ { diff --git a/pkg/client/request.go b/pkg/client/request.go index cb440ed02dc..ca0fb9aadc9 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -103,6 +103,7 @@ type Request struct { // structural elements of the request that are part of the Kubernetes API conventions namespace string + namespaceSet bool resource string resourceName string selector labels.Selector @@ -190,10 +191,11 @@ func (r *Request) Namespace(namespace string) *Request { if r.err != nil { return r } - if len(r.namespace) != 0 { + if r.namespaceSet { r.err = fmt.Errorf("namespace already set to %q, cannot change to %q", r.namespace, namespace) return r } + r.namespaceSet = true r.namespace = namespace return r } @@ -330,7 +332,7 @@ func (r *Request) Poller(poller PollFunc) *Request { func (r *Request) finalURL() string { p := r.path - if !r.namespaceInQuery { + if r.namespaceSet && !r.namespaceInQuery && len(r.namespace) > 0 { p = path.Join(p, "ns", r.namespace) } if len(r.resource) != 0 { @@ -353,7 +355,7 @@ func (r *Request) finalURL() string { query.Add(key, value) } - if r.namespaceInQuery && len(r.namespace) > 0 { + if r.namespaceSet && r.namespaceInQuery && len(r.namespace) > 0 { query.Add("namespace", r.namespace) } diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index 2891615187a..3f96e02d4a1 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -86,6 +86,11 @@ func TestRequestAbsPathPreservesTrailingSlash(t *testing.T) { if s := r.finalURL(); s != "/foo/" { t.Errorf("trailing slash should be preserved: %s", s) } + + r = (&Request{baseURL: &url.URL{}}).AbsPath("/foo/") + if s := r.finalURL(); s != "/foo/" { + t.Errorf("trailing slash should be preserved: %s", s) + } } func TestRequestAbsPathJoins(t *testing.T) { diff --git a/pkg/client/restclient_test.go b/pkg/client/restclient_test.go index 31756d72adb..d0e2e1ebb6f 100644 --- a/pkg/client/restclient_test.go +++ b/pkg/client/restclient_test.go @@ -27,6 +27,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -40,7 +41,8 @@ func TestSetsCodec(t *testing.T) { "v1beta1": {false, "/api/v1beta1/", v1beta1.Codec}, "": {false, "/api/v1beta1/", v1beta1.Codec}, "v1beta2": {false, "/api/v1beta2/", v1beta2.Codec}, - "v1beta3": {true, "", nil}, + "v1beta3": {false, "/api/v1beta3/", v1beta3.Codec}, + "v1beta4": {true, "", nil}, } for version, expected := range testCases { client, err := New(&Config{Host: "127.0.0.1", Version: version}) diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index 4ae2755b382..753d95c74f2 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -457,6 +457,10 @@ func (c *Converter) defaultConvert(sv, dv reflect.Value, scope *scope) error { } dkv := reflect.New(dt.Elem()).Elem() scope.setKeys(sk.Interface(), dk.Interface()) + // TODO: sv.MapIndex(sk) may return a value with CanAddr() == false, + // because a map[string]struct{} does not allow a pointer reference. + // Calling a custom conversion function defined for the map value + // will panic. Example is PodInfo map[string]ContainerStatus. if err := c.convert(sv.MapIndex(sk), dkv, scope); err != nil { return err } diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index 0f5b58200f7..f108e711414 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -21,9 +21,13 @@ import ( "fmt" "net/http" "reflect" + "strings" "testing" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) // Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. @@ -50,3 +54,31 @@ func TestGetUnknownSchemaObject(t *testing.T) { t.Errorf("unexpected output: %s", buf.String()) } } + +// Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. +func TestGetSchemaObject(t *testing.T) { + f, tf, _ := NewTestFactory() + f.Mapper = latest.RESTMapper + f.Typer = api.Scheme + codec := latest.Codec + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Resp: &http.Response{StatusCode: 200, Body: objBody(codec, &api.ReplicationController{ObjectMeta: api.ObjectMeta{Name: "foo"}})}, + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.Flags().String("api-version", "v1beta3", "") + cmd.Flags().String("namespace", "test", "") + cmd.Run(cmd, []string{"replicationcontrollers", "foo"}) + + expected := &api.ReplicationController{ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ReplicationControllerSpec{Template: &api.PodTemplateSpec{}}} + actual := tf.Printer.(*testPrinter).Obj + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %s", util.ObjectGoPrintDiff(expected, actual)) + } + if !strings.Contains(buf.String(), "\"foo\"") { + t.Errorf("unexpected output: %s", buf.String()) + } +} diff --git a/pkg/master/master.go b/pkg/master/master.go index 1ad09d5c542..2329e959ce8 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -33,6 +33,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" @@ -72,6 +73,7 @@ type Config struct { EnableLogsSupport bool EnableUISupport bool EnableSwaggerSupport bool + EnableV1Beta3 bool APIPrefix string CorsAllowedOriginList util.StringList Authenticator authenticator.Request @@ -122,6 +124,7 @@ type Master struct { authorizer authorizer.Authorizer admissionControl admission.Interface masterCount int + v1beta3 bool readOnlyServer string readWriteServer string @@ -252,6 +255,7 @@ func New(c *Config) *Master { authenticator: c.Authenticator, authorizer: c.Authorizer, admissionControl: c.AdmissionControl, + v1beta3: c.EnableV1Beta3, masterCount: c.MasterCount, readOnlyServer: net.JoinHostPort(c.PublicAddress, strconv.Itoa(int(c.ReadOnlyPort))), @@ -353,11 +357,16 @@ func (m *Master) init(c *Config) { "bindings": binding.NewREST(m.bindingRegistry), } + versionHandler := apiserver.APIVersionHandler("v1beta1", "v1beta2") + apiserver.NewAPIGroupVersion(m.API_v1beta1()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta1") apiserver.NewAPIGroupVersion(m.API_v1beta2()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta2") + if c.EnableV1Beta3 { + apiserver.NewAPIGroupVersion(m.API_v1beta3()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta3") + versionHandler = apiserver.APIVersionHandler("v1beta1", "v1beta2", "v1beta3") + } // TODO: InstallREST should register each version automatically - versionHandler := apiserver.APIVersionHandler("v1beta1", "v1beta2") m.rootWebService.Route(m.rootWebService.GET(c.APIPrefix).To(versionHandler)) apiserver.InstallSupport(m.handlerContainer, m.rootWebService) @@ -482,3 +491,15 @@ func (m *Master) API_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, } return storage, v1beta2.Codec, "/api/v1beta2", latest.SelfLinker, m.admissionControl } + +// API_v1beta3 returns the resources and codec for API version v1beta3. +func (m *Master) API_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.Interface) { + storage := make(map[string]apiserver.RESTStorage) + for k, v := range m.storage { + if k == "minions" { + continue + } + storage[strings.ToLower(k)] = v + } + return storage, v1beta3.Codec, "/api/v1beta3", latest.SelfLinker, m.admissionControl +} diff --git a/pkg/util/configuration_map.go b/pkg/util/configuration_map.go new file mode 100644 index 00000000000..a27a9844314 --- /dev/null +++ b/pkg/util/configuration_map.go @@ -0,0 +1,53 @@ +/* +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 util + +import ( + "fmt" + "sort" + "strings" +) + +type ConfigurationMap map[string]string + +func (m *ConfigurationMap) String() string { + pairs := []string{} + for k, v := range *m { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(pairs) + return strings.Join(pairs, ",") +} + +func (m *ConfigurationMap) Set(value string) error { + for _, s := range strings.Split(value, ",") { + if len(s) == 0 { + continue + } + arr := strings.SplitN(s, "=", 2) + if len(arr) == 2 { + (*m)[arr[0]] = arr[1] + } else { + (*m)[arr[0]] = "" + } + } + return nil +} + +func (*ConfigurationMap) Type() string { + return "mapStringString" +}