diff --git a/pkg/expapi/register.go b/pkg/expapi/register.go index 4198ad56282..581b1d315c1 100644 --- a/pkg/expapi/register.go +++ b/pkg/expapi/register.go @@ -16,4 +16,19 @@ limitations under the License. package expapi -func init() {} +import ( + "k8s.io/kubernetes/pkg/api" +) + +func init() { + // Register the API. + addKnownTypes() +} + +// Adds the list of known types to api.Scheme. +func addKnownTypes() { + api.Scheme.AddKnownTypes("", &Scale{}, &ReplicationControllerDummy{}) +} + +func (*Scale) IsAnAPIObject() {} +func (*ReplicationControllerDummy) IsAnAPIObject() {} diff --git a/pkg/expapi/types.go b/pkg/expapi/types.go index f30be24d406..81ee83685ce 100644 --- a/pkg/expapi/types.go +++ b/pkg/expapi/types.go @@ -27,3 +27,37 @@ support is experimental. */ package expapi + +import "k8s.io/kubernetes/pkg/api" + +// ScaleSpec describes the attributes a Scale subresource +type ScaleSpec struct { + // Replicas is the number of desired replicas. + Replicas int `json:"replicas,omitempty" description:"number of replicas desired; http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md#what-is-a-replication-controller"` +} + +// ScaleStatus represents the current status of a Scale subresource. +type ScaleStatus struct { + // Replicas is the number of actual replicas. + Replicas int `json:"replicas" description:"most recently oberved number of replicas; see http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md#what-is-a-replication-controller"` + + // Selector is a label query over pods that should match the replicas count. + Selector map[string]string `json:"selector,omitempty" description:"label keys and values that must match in order to be controlled by this replication controller, if empty defaulted to labels on Pod template; see http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors"` +} + +// Scale subresource, applicable to ReplicationControllers and (in future) Deployment. +type Scale struct { + api.TypeMeta `json:",inline"` + api.ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata"` + + // Spec defines the behavior of the scaler. + Spec ScaleSpec `json:"spec,omitempty" description:"specification of the desired behavior of the scaler; http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status"` + + // Status represents the current status of the scaler. + Status ScaleStatus `json:"status,omitempty" description:"most recently observed status of the service; populated by the system, read-only; http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status"` +} + +// Dummy definition +type ReplicationControllerDummy struct { + api.TypeMeta `json:",inline"` +} diff --git a/pkg/expapi/v1/defaults.go b/pkg/expapi/v1/defaults.go index 268770da898..d4a06d1bcf9 100644 --- a/pkg/expapi/v1/defaults.go +++ b/pkg/expapi/v1/defaults.go @@ -16,4 +16,5 @@ limitations under the License. package v1 -func addDefaultingFuncs() {} +func addDefaultingFuncs() { +} diff --git a/pkg/expapi/v1/register.go b/pkg/expapi/v1/register.go index aaa8d4c0603..882359d7b4c 100644 --- a/pkg/expapi/v1/register.go +++ b/pkg/expapi/v1/register.go @@ -27,4 +27,13 @@ func init() { addDeepCopyFuncs() addConversionFuncs() addDefaultingFuncs() + addKnownTypes() } + +// Adds the list of known types to api.Scheme. +func addKnownTypes() { + api.Scheme.AddKnownTypes("v1", &Scale{}, &ReplicationControllerDummy{}) +} + +func (*Scale) IsAnAPIObject() {} +func (*ReplicationControllerDummy) IsAnAPIObject() {} diff --git a/pkg/expapi/v1/types.go b/pkg/expapi/v1/types.go index 2a5c8ca4cba..a0eac74ab65 100644 --- a/pkg/expapi/v1/types.go +++ b/pkg/expapi/v1/types.go @@ -15,3 +15,37 @@ limitations under the License. */ package v1 + +import "k8s.io/kubernetes/pkg/api/v1" + +// ScaleSpec describes the attributes a Scale subresource +type ScaleSpec struct { + // Replicas is the number of desired replicas. + Replicas int `json:"replicas,omitempty" description:"number of replicas desired; see http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md#what-is-a-replication-controller"` +} + +// ScaleStatus represents the current status of a Scale subresource. +type ScaleStatus struct { + // Replicas is the number of actual replicas. + Replicas int `json:"replicas" description:"most recently oberved number of replicas; see http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md#what-is-a-replication-controller"` + + // Selector is a label query over pods that should match the replicas count. + Selector map[string]string `json:"selector,omitempty" description:"label keys and values that must match in order to be controlled by this replication controller, if empty defaulted to labels on Pod template; see http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors"` +} + +// Scale subresource, applicable to ReplicationControllers and (in future) Deployment. +type Scale struct { + v1.TypeMeta `json:",inline"` + v1.ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata"` + + // Spec defines the behavior of the scaler. + Spec ScaleSpec `json:"spec,omitempty" description:"specification of the desired behavior of the scaler; http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status"` + + // Status represents the current status of the scaler. + Status ScaleStatus `json:"status,omitempty" description:"most recently observed status of the service; populated by the system, read-only; http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status"` +} + +// Dummy definition +type ReplicationControllerDummy struct { + v1.TypeMeta `json:",inline"` +} diff --git a/pkg/master/master.go b/pkg/master/master.go index 85bb7cdf8ef..1b036bc4eb2 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -55,6 +55,7 @@ import ( endpointsetcd "k8s.io/kubernetes/pkg/registry/endpoint/etcd" "k8s.io/kubernetes/pkg/registry/etcd" "k8s.io/kubernetes/pkg/registry/event" + expcontrolleretcd "k8s.io/kubernetes/pkg/registry/experimental/controller/etcd" "k8s.io/kubernetes/pkg/registry/limitrange" "k8s.io/kubernetes/pkg/registry/minion" nodeetcd "k8s.io/kubernetes/pkg/registry/minion/etcd" @@ -777,7 +778,13 @@ func (m *Master) api_v1() *apiserver.APIGroupVersion { // expapi returns the resources and codec for the experimental api func (m *Master) expapi(c *Config) *apiserver.APIGroupVersion { - storage := map[string]rest.Storage{} + + controllerStorage := expcontrolleretcd.NewStorage(c.DatabaseStorage) + storage := map[string]rest.Storage{ + strings.ToLower("replicationControllers"): controllerStorage.ReplicationController, + strings.ToLower("replicationControllers/scaler"): controllerStorage.Scale, + } + return &apiserver.APIGroupVersion{ Root: m.expAPIPrefix, diff --git a/pkg/registry/experimental/controller/etcd/etcd.go b/pkg/registry/experimental/controller/etcd/etcd.go new file mode 100644 index 00000000000..6937077c210 --- /dev/null +++ b/pkg/registry/experimental/controller/etcd/etcd.go @@ -0,0 +1,120 @@ +/* +Copyright 2014 The Kubernetes Authors 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 etcd + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" + + "k8s.io/kubernetes/pkg/registry/controller" + "k8s.io/kubernetes/pkg/registry/controller/etcd" + + "k8s.io/kubernetes/pkg/expapi" +) + +// Container includes dummy storage for RC pods and experimental storage for Scale. +type ContainerStorage struct { + ReplicationController *RcREST + Scale *ScaleREST +} + +func NewStorage(s storage.Interface) ContainerStorage { + rcRegistry := controller.NewRegistry(etcd.NewREST(s)) + + return ContainerStorage{ + ReplicationController: &RcREST{}, + Scale: &ScaleREST{registry: &rcRegistry}, + } +} + +type ScaleREST struct { + registry *controller.Registry +} + +// LogREST implements GetterWithOptions +var _ = rest.Patcher(&ScaleREST{}) + +// New creates a new Scale object +func (r *ScaleREST) New() runtime.Object { + return &expapi.Scale{} +} + +func (r *ScaleREST) Get(ctx api.Context, name string) (runtime.Object, error) { + rc, err := (*r.registry).GetController(ctx, name) + if err != nil { + return nil, errors.NewNotFound("scaler", name) + } + return &expapi.Scale{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: rc.Namespace, + CreationTimestamp: rc.CreationTimestamp, + }, + Spec: expapi.ScaleSpec{ + Replicas: rc.Spec.Replicas, + }, + Status: expapi.ScaleStatus{ + Replicas: rc.Status.Replicas, + Selector: rc.Spec.Selector, + }, + }, nil +} + +func (r *ScaleREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + if obj == nil { + return nil, false, errors.NewBadRequest(fmt.Sprintf("nil update passed to Scale")) + } + scaler, ok := obj.(*expapi.Scale) + if !ok { + return nil, false, errors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj)) + } + rc, err := (*r.registry).GetController(ctx, scaler.Name) + if err != nil { + return nil, false, errors.NewNotFound("scaler", scaler.Name) + } + rc.Spec.Replicas = scaler.Spec.Replicas + rc, err = (*r.registry).UpdateController(ctx, rc) + if err != nil { + return nil, false, errors.NewConflict("scaler", scaler.Name, err) + } + return &expapi.Scale{ + ObjectMeta: api.ObjectMeta{ + Name: rc.Name, + Namespace: rc.Namespace, + CreationTimestamp: rc.CreationTimestamp, + }, + Spec: expapi.ScaleSpec{ + Replicas: rc.Spec.Replicas, + }, + Status: expapi.ScaleStatus{ + Replicas: rc.Status.Replicas, + Selector: rc.Spec.Selector, + }, + }, false, nil +} + +// Dummy implementation +type RcREST struct{} + +func (r *RcREST) New() runtime.Object { + return &expapi.ReplicationControllerDummy{} +} diff --git a/pkg/registry/experimental/controller/etcd/etcd_test.go b/pkg/registry/experimental/controller/etcd/etcd_test.go new file mode 100644 index 00000000000..1ca3e1acf3f --- /dev/null +++ b/pkg/registry/experimental/controller/etcd/etcd_test.go @@ -0,0 +1,153 @@ +/* +Copyright 2014 The Kubernetes Authors 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 etcd + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/latest" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" + etcdstorage "k8s.io/kubernetes/pkg/storage/etcd" + "k8s.io/kubernetes/pkg/tools" + "k8s.io/kubernetes/pkg/tools/etcdtest" + "k8s.io/kubernetes/pkg/util" + + "k8s.io/kubernetes/pkg/expapi" + + "github.com/coreos/go-etcd/etcd" +) + +func newEtcdStorage(t *testing.T) (*tools.FakeEtcdClient, storage.Interface) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + etcdStorage := etcdstorage.NewEtcdStorage(fakeEtcdClient, latest.Codec, etcdtest.PathPrefix()) + return fakeEtcdClient, etcdStorage +} + +func newStorage(t *testing.T) (*RcREST, *ScaleREST, *tools.FakeEtcdClient, storage.Interface) { + fakeEtcdClient, etcdStorage := newEtcdStorage(t) + storage := NewStorage(etcdStorage) + return storage.ReplicationController, storage.Scale, fakeEtcdClient, etcdStorage +} + +var validPodTemplate = api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "test", + Image: "test_image", + ImagePullPolicy: api.PullIfNotPresent, + }, + }, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + }, + }, +} + +var validReplicas = 8 + +var validControllerSpec = api.ReplicationControllerSpec{ + Replicas: validReplicas, + Selector: validPodTemplate.Template.Labels, + Template: &validPodTemplate.Template, +} + +var validController = api.ReplicationController{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "1"}, + Spec: validControllerSpec, +} + +var validScale = expapi.Scale{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test"}, + Spec: expapi.ScaleSpec{ + Replicas: validReplicas, + }, + Status: expapi.ScaleStatus{ + Replicas: 0, + Selector: validPodTemplate.Template.Labels, + }, +} + +func TestGet(t *testing.T) { + expect := &validScale + + fakeEtcdClient, etcdStorage := newEtcdStorage(t) + + key := etcdtest.AddPrefix("/controllers/test/foo") + fakeEtcdClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, &validController), + ModifiedIndex: 1, + }, + }, + } + storage := NewStorage(etcdStorage).Scale + + obj, err := storage.Get(api.WithNamespace(api.NewContext(), "test"), "foo") + scaler := obj.(*expapi.Scale) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if e, a := expect, scaler; !api.Semantic.DeepEqual(e, a) { + t.Errorf("Unexpected scaler: %s", util.ObjectDiff(e, a)) + } +} + +func TestUpdate(t *testing.T) { + fakeEtcdClient, etcdStorage := newEtcdStorage(t) + storage := NewStorage(etcdStorage).Scale + + key := etcdtest.AddPrefix("/controllers/test/foo") + fakeEtcdClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, &validController), + ModifiedIndex: 1, + }, + }, + } + replicas := 12 + update := expapi.Scale{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test"}, + Spec: expapi.ScaleSpec{ + Replicas: replicas, + }, + } + + _, _, err := storage.Update(api.WithNamespace(api.NewContext(), "test"), &update) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + response, err := fakeEtcdClient.Get(key, false, false) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + var controller api.ReplicationController + latest.Codec.DecodeInto([]byte(response.Node.Value), &controller) + if controller.Spec.Replicas != replicas { + t.Errorf("wrong replicas count expected: %d got: %d", replicas, controller.Spec.Replicas) + } +}