diff --git a/pkg/master/master.go b/pkg/master/master.go index e076014dd47..fba5eff8e1a 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -826,7 +826,7 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion { autoscalerStorage := horizontalpodautoscaleretcd.NewREST(c.ExpDatabaseStorage) thirdPartyResourceStorage := thirdpartyresourceetcd.NewREST(c.ExpDatabaseStorage) daemonSetStorage := daemonetcd.NewREST(c.ExpDatabaseStorage) - deploymentStorage := deploymentetcd.NewREST(c.ExpDatabaseStorage) + deploymentStorage := deploymentetcd.NewStorage(c.ExpDatabaseStorage) jobStorage := jobetcd.NewREST(c.ExpDatabaseStorage) storage := map[string]rest.Storage{ @@ -835,7 +835,8 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion { strings.ToLower("horizontalpodautoscalers"): autoscalerStorage, strings.ToLower("thirdpartyresources"): thirdPartyResourceStorage, strings.ToLower("daemonsets"): daemonSetStorage, - strings.ToLower("deployments"): deploymentStorage, + strings.ToLower("deployments"): deploymentStorage.Deployment, + strings.ToLower("deployments/scale"): deploymentStorage.Scale, strings.ToLower("jobs"): jobStorage, } diff --git a/pkg/registry/deployment/etcd/etcd.go b/pkg/registry/deployment/etcd/etcd.go index 8eace5bcfc6..ddb576a51d5 100644 --- a/pkg/registry/deployment/etcd/etcd.go +++ b/pkg/registry/deployment/etcd/etcd.go @@ -17,7 +17,11 @@ 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/apis/experimental" "k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/labels" @@ -28,6 +32,22 @@ import ( "k8s.io/kubernetes/pkg/storage" ) +// DeploymentStorage includes dummy storage for Deployments and for Scale subresource. +type DeploymentStorage struct { + Deployment *REST + Scale *ScaleREST +} + +func NewStorage(s storage.Interface) DeploymentStorage { + deploymentRest := NewREST(s) + deploymentRegistry := deployment.NewRegistry(deploymentRest) + + return DeploymentStorage{ + Deployment: deploymentRest, + Scale: &ScaleREST{registry: &deploymentRegistry}, + } +} + type REST struct { *etcdgeneric.Etcd } @@ -69,3 +89,69 @@ func NewREST(s storage.Interface) *REST { } return &REST{store} } + +type ScaleREST struct { + registry *deployment.Registry +} + +// ScaleREST implements Patcher +var _ = rest.Patcher(&ScaleREST{}) + +// New creates a new Scale object +func (r *ScaleREST) New() runtime.Object { + return &experimental.Scale{} +} + +func (r *ScaleREST) Get(ctx api.Context, name string) (runtime.Object, error) { + deployment, err := (*r.registry).GetDeployment(ctx, name) + if err != nil { + return nil, errors.NewNotFound("scale", name) + } + return &experimental.Scale{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: deployment.Namespace, + CreationTimestamp: deployment.CreationTimestamp, + }, + Spec: experimental.ScaleSpec{ + Replicas: deployment.Spec.Replicas, + }, + Status: experimental.ScaleStatus{ + Replicas: deployment.Status.Replicas, + Selector: deployment.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")) + } + scale, ok := obj.(*experimental.Scale) + if !ok { + return nil, false, errors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj)) + } + deployment, err := (*r.registry).GetDeployment(ctx, scale.Name) + if err != nil { + return nil, false, errors.NewNotFound("scale", scale.Name) + } + deployment.Spec.Replicas = scale.Spec.Replicas + deployment, err = (*r.registry).UpdateDeployment(ctx, deployment) + if err != nil { + return nil, false, errors.NewConflict("scale", scale.Name, err) + } + return &experimental.Scale{ + ObjectMeta: api.ObjectMeta{ + Name: deployment.Name, + Namespace: deployment.Namespace, + CreationTimestamp: deployment.CreationTimestamp, + }, + Spec: experimental.ScaleSpec{ + Replicas: deployment.Spec.Replicas, + }, + Status: experimental.ScaleStatus{ + Replicas: deployment.Status.Replicas, + Selector: deployment.Spec.Selector, + }, + }, false, nil +} diff --git a/pkg/registry/deployment/etcd/etcd_test.go b/pkg/registry/deployment/etcd/etcd_test.go index e11d113a633..23d57cb623f 100755 --- a/pkg/registry/deployment/etcd/etcd_test.go +++ b/pkg/registry/deployment/etcd/etcd_test.go @@ -20,24 +20,31 @@ import ( "testing" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/apis/experimental" "k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/registry/registrytest" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/tools" + "k8s.io/kubernetes/pkg/tools/etcdtest" + "k8s.io/kubernetes/pkg/util" ) -func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient) { +func newStorage(t *testing.T) (*DeploymentStorage, *tools.FakeEtcdClient) { etcdStorage, fakeClient := registrytest.NewEtcdStorage(t, "experimental") - return NewREST(etcdStorage), fakeClient + deploymentStorage := NewStorage(etcdStorage) + return &deploymentStorage, fakeClient } +var namespace = "foo-namespace" +var name = "foo-deployment" + func validNewDeployment() *experimental.Deployment { return &experimental.Deployment{ ObjectMeta: api.ObjectMeta{ - Name: "foo", - Namespace: api.NamespaceDefault, + Name: name, + Namespace: namespace, }, Spec: experimental.DeploymentSpec{ Selector: map[string]string{"a": "b"}, @@ -58,15 +65,34 @@ func validNewDeployment() *experimental.Deployment { }, }, UniqueLabelKey: "my-label", + Replicas: 7, + }, + Status: experimental.DeploymentStatus{ + Replicas: 5, }, } } var validDeployment = *validNewDeployment() +func validNewScale() *experimental.Scale { + return &experimental.Scale{ + ObjectMeta: api.ObjectMeta{Name: name, Namespace: namespace}, + Spec: experimental.ScaleSpec{ + Replicas: validDeployment.Spec.Replicas, + }, + Status: experimental.ScaleStatus{ + Replicas: validDeployment.Status.Replicas, + Selector: validDeployment.Spec.Template.Labels, + }, + } +} + +var validScale = *validNewScale() + func TestCreate(t *testing.T) { storage, fakeClient := newStorage(t) - test := registrytest.New(t, fakeClient, storage.Etcd) + test := registrytest.New(t, fakeClient, storage.Deployment.Etcd) deployment := validNewDeployment() deployment.ObjectMeta = api.ObjectMeta{} test.TestCreate( @@ -84,7 +110,7 @@ func TestCreate(t *testing.T) { func TestUpdate(t *testing.T) { storage, fakeClient := newStorage(t) - test := registrytest.New(t, fakeClient, storage.Etcd) + test := registrytest.New(t, fakeClient, storage.Deployment.Etcd) test.TestUpdate( // valid validNewDeployment(), @@ -120,25 +146,25 @@ func TestUpdate(t *testing.T) { func TestDelete(t *testing.T) { storage, fakeClient := newStorage(t) - test := registrytest.New(t, fakeClient, storage.Etcd) + test := registrytest.New(t, fakeClient, storage.Deployment.Etcd) test.TestDelete(validNewDeployment()) } func TestGet(t *testing.T) { storage, fakeClient := newStorage(t) - test := registrytest.New(t, fakeClient, storage.Etcd) + test := registrytest.New(t, fakeClient, storage.Deployment.Etcd) test.TestGet(validNewDeployment()) } func TestList(t *testing.T) { storage, fakeClient := newStorage(t) - test := registrytest.New(t, fakeClient, storage.Etcd) + test := registrytest.New(t, fakeClient, storage.Deployment.Etcd) test.TestList(validNewDeployment()) } func TestWatch(t *testing.T) { storage, fakeClient := newStorage(t) - test := registrytest.New(t, fakeClient, storage.Etcd) + test := registrytest.New(t, fakeClient, storage.Deployment.Etcd) test.TestWatch( validNewDeployment(), // matching labels @@ -150,12 +176,63 @@ func TestWatch(t *testing.T) { }, // matching fields []fields.Set{ - {"metadata.name": "foo"}, + {"metadata.name": name}, }, // not matchin fields []fields.Set{ {"metadata.name": "bar"}, - {"name": "foo"}, + {"name": name}, }, ) } + +func TestScaleGet(t *testing.T) { + storage, fakeClient := newStorage(t) + + ctx := api.WithNamespace(api.NewContext(), namespace) + key := etcdtest.AddPrefix("/deployments/" + namespace + "/" + name) + if _, err := fakeClient.Set(key, runtime.EncodeOrDie(testapi.Experimental.Codec(), &validDeployment), 0); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expect := &validScale + obj, err := storage.Scale.Get(ctx, name) + scale := obj.(*experimental.Scale) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if e, a := expect, scale; !api.Semantic.DeepEqual(e, a) { + t.Errorf("unexpected scale: %s", util.ObjectDiff(e, a)) + } +} + +func TestScaleUpdate(t *testing.T) { + storage, fakeClient := newStorage(t) + + ctx := api.WithNamespace(api.NewContext(), namespace) + key := etcdtest.AddPrefix("/deployments/" + namespace + "/" + name) + if _, err := fakeClient.Set(key, runtime.EncodeOrDie(testapi.Experimental.Codec(), &validDeployment), 0); err != nil { + t.Fatalf("unexpected error: %v", err) + } + replicas := 12 + update := experimental.Scale{ + ObjectMeta: api.ObjectMeta{Name: name, Namespace: namespace}, + Spec: experimental.ScaleSpec{ + Replicas: replicas, + }, + } + + if _, _, err := storage.Scale.Update(ctx, &update); err != nil { + t.Fatalf("unexpected error: %v", err) + } + response, err := fakeClient.Get(key, false, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var deployment experimental.Deployment + testapi.Experimental.Codec().DecodeInto([]byte(response.Node.Value), &deployment) + if deployment.Spec.Replicas != replicas { + t.Errorf("wrong replicas count expected: %d got: %d", replicas, deployment.Spec.Replicas) + } +} diff --git a/pkg/registry/deployment/registry.go b/pkg/registry/deployment/registry.go new file mode 100644 index 00000000000..fcf324ce2ae --- /dev/null +++ b/pkg/registry/deployment/registry.go @@ -0,0 +1,87 @@ +/* +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 deployment + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/apis/experimental" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" +) + +// Registry is an interface for things that know how to store Deployments. +type Registry interface { + ListDeployments(ctx api.Context, label labels.Selector, field fields.Selector) (*experimental.DeploymentList, error) + GetDeployment(ctx api.Context, deploymentID string) (*experimental.Deployment, error) + CreateDeployment(ctx api.Context, deployment *experimental.Deployment) (*experimental.Deployment, error) + UpdateDeployment(ctx api.Context, deployment *experimental.Deployment) (*experimental.Deployment, error) + DeleteDeployment(ctx api.Context, deploymentID string) error +} + +// storage puts strong typing around storage calls +type storage struct { + rest.StandardStorage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched types will panic. +func NewRegistry(s rest.StandardStorage) Registry { + return &storage{s} +} + +// List obtains a list of Deployments that match selector. +func (s *storage) ListDeployments(ctx api.Context, label labels.Selector, field fields.Selector) (*experimental.DeploymentList, error) { + if !field.Empty() { + return nil, fmt.Errorf("field selector not supported yet") + } + obj, err := s.List(ctx, label, field) + if err != nil { + return nil, err + } + return obj.(*experimental.DeploymentList), err +} + +func (s *storage) GetDeployment(ctx api.Context, deploymentID string) (*experimental.Deployment, error) { + obj, err := s.Get(ctx, deploymentID) + if err != nil { + return nil, err + } + return obj.(*experimental.Deployment), nil +} + +func (s *storage) CreateDeployment(ctx api.Context, deployment *experimental.Deployment) (*experimental.Deployment, error) { + obj, err := s.Create(ctx, deployment) + if err != nil { + return nil, err + } + return obj.(*experimental.Deployment), nil +} + +func (s *storage) UpdateDeployment(ctx api.Context, deployment *experimental.Deployment) (*experimental.Deployment, error) { + obj, _, err := s.Update(ctx, deployment) + if err != nil { + return nil, err + } + return obj.(*experimental.Deployment), nil +} + +func (s *storage) DeleteDeployment(ctx api.Context, deploymentID string) error { + _, err := s.Delete(ctx, deploymentID, nil) + return err +} diff --git a/pkg/registry/experimental/controller/etcd/etcd.go b/pkg/registry/experimental/controller/etcd/etcd.go index 134f42f457f..a05b2d4d314 100644 --- a/pkg/registry/experimental/controller/etcd/etcd.go +++ b/pkg/registry/experimental/controller/etcd/etcd.go @@ -50,7 +50,7 @@ type ScaleREST struct { registry *controller.Registry } -// LogREST implements GetterWithOptions +// ScaleREST implements Patcher var _ = rest.Patcher(&ScaleREST{}) // New creates a new Scale object