Merge pull request #49168 from crimsonfaith91/apps-v1beta2

Automatic merge from submit-queue

StatefulSet scale subresource

**What this PR does / why we need it**: This PR implements scale subresource for StatefulSet.

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #46005

**Special notes for your reviewer**:

**Release note**:

```release-note
StatefulSet uses scale subresource when scaling in accord with ReplicationController, ReplicaSet, and Deployment implementations.
```
**Feature Checklist**:
- [x] Introduce Registry interface for storage purpose
- [x] Introduce `ScaleREST New(), Get() and Update()` utility functions
- [x] Create a `ScaleREST` object at `NewREST()` and return it
- [x] Enable scale subresource by adding `/scale` field to the storage map

**Testing Checklist**:
- Unit testing
  - [x] Modify `newStorage()` to call `NewStorage()`, and change all unit tests accordingly
  - [x] Add unit tests for `ScaleREST Get() and Update()` utility functions
  - [x] Add missing unit test for `ShortNames`

- Manual testing
  - [x] Verify existence of the subresource using `kubectl proxy` command
  - [x] Modify the subresource using `curl` via `POST`

- e2e testing
  - [x] Add e2e tests using `RESTClient`
pull/6/head
Kubernetes Submit Queue 2017-08-07 17:05:24 -07:00 committed by GitHub
commit 0967f9560a
14 changed files with 2870 additions and 1102 deletions

View File

@ -21339,6 +21339,160 @@
}
]
},
"/apis/apps/v1beta1/namespaces/{namespace}/statefulsets/{name}/scale": {
"get": {
"description": "read scale of the specified StatefulSet",
"consumes": [
"*/*"
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"schemes": [
"https"
],
"tags": [
"apps_v1beta1"
],
"operationId": "readAppsV1beta1NamespacedStatefulSetScale",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta1.Scale"
}
},
"401": {
"description": "Unauthorized"
}
},
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "apps",
"kind": "Scale",
"version": "v1beta1"
}
},
"put": {
"description": "replace scale of the specified StatefulSet",
"consumes": [
"*/*"
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"schemes": [
"https"
],
"tags": [
"apps_v1beta1"
],
"operationId": "replaceAppsV1beta1NamespacedStatefulSetScale",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta1.Scale"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta1.Scale"
}
},
"401": {
"description": "Unauthorized"
}
},
"x-kubernetes-action": "put",
"x-kubernetes-group-version-kind": {
"group": "apps",
"kind": "Scale",
"version": "v1beta1"
}
},
"patch": {
"description": "partially update scale of the specified StatefulSet",
"consumes": [
"application/json-patch+json",
"application/merge-patch+json",
"application/strategic-merge-patch+json"
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"schemes": [
"https"
],
"tags": [
"apps_v1beta1"
],
"operationId": "patchAppsV1beta1NamespacedStatefulSetScale",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta1.Scale"
}
},
"401": {
"description": "Unauthorized"
}
},
"x-kubernetes-action": "patch",
"x-kubernetes-group-version-kind": {
"group": "apps",
"kind": "Scale",
"version": "v1beta1"
}
},
"parameters": [
{
"uniqueItems": true,
"type": "string",
"description": "name of the Scale",
"name": "name",
"in": "path",
"required": true
},
{
"uniqueItems": true,
"type": "string",
"description": "object name and auth scope, such as for teams and projects",
"name": "namespace",
"in": "path",
"required": true
},
{
"uniqueItems": true,
"type": "string",
"description": "If 'true', then the output is pretty printed.",
"name": "pretty",
"in": "query"
}
]
},
"/apis/apps/v1beta1/namespaces/{namespace}/statefulsets/{name}/status": {
"get": {
"description": "read status of the specified StatefulSet",
@ -25360,6 +25514,160 @@
}
]
},
"/apis/apps/v1beta2/namespaces/{namespace}/statefulsets/{name}/scale": {
"get": {
"description": "read scale of the specified StatefulSet",
"consumes": [
"*/*"
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"schemes": [
"https"
],
"tags": [
"apps_v1beta2"
],
"operationId": "readAppsV1beta2NamespacedStatefulSetScale",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta2.Scale"
}
},
"401": {
"description": "Unauthorized"
}
},
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "apps",
"kind": "Scale",
"version": "v1beta2"
}
},
"put": {
"description": "replace scale of the specified StatefulSet",
"consumes": [
"*/*"
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"schemes": [
"https"
],
"tags": [
"apps_v1beta2"
],
"operationId": "replaceAppsV1beta2NamespacedStatefulSetScale",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta2.Scale"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta2.Scale"
}
},
"401": {
"description": "Unauthorized"
}
},
"x-kubernetes-action": "put",
"x-kubernetes-group-version-kind": {
"group": "apps",
"kind": "Scale",
"version": "v1beta2"
}
},
"patch": {
"description": "partially update scale of the specified StatefulSet",
"consumes": [
"application/json-patch+json",
"application/merge-patch+json",
"application/strategic-merge-patch+json"
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"schemes": [
"https"
],
"tags": [
"apps_v1beta2"
],
"operationId": "patchAppsV1beta2NamespacedStatefulSetScale",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/io.k8s.api.apps.v1beta2.Scale"
}
},
"401": {
"description": "Unauthorized"
}
},
"x-kubernetes-action": "patch",
"x-kubernetes-group-version-kind": {
"group": "apps",
"kind": "Scale",
"version": "v1beta2"
}
},
"parameters": [
{
"uniqueItems": true,
"type": "string",
"description": "name of the Scale",
"name": "name",
"in": "path",
"required": true
},
{
"uniqueItems": true,
"type": "string",
"description": "object name and auth scope, such as for teams and projects",
"name": "namespace",
"in": "path",
"required": true
},
{
"uniqueItems": true,
"type": "string",
"description": "If 'true', then the output is pretty printed.",
"name": "pretty",
"in": "query"
}
]
},
"/apis/apps/v1beta2/namespaces/{namespace}/statefulsets/{name}/status": {
"get": {
"description": "read status of the specified StatefulSet",

View File

@ -2982,6 +2982,171 @@
}
]
},
{
"path": "/apis/apps/v1beta1/namespaces/{namespace}/statefulsets/{name}/scale",
"description": "API at /apis/apps/v1beta1",
"operations": [
{
"type": "v1beta1.Scale",
"method": "GET",
"summary": "read scale of the specified StatefulSet",
"nickname": "readNamespacedStatefulSetScale",
"parameters": [
{
"type": "string",
"paramType": "query",
"name": "pretty",
"description": "If 'true', then the output is pretty printed.",
"required": false,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "namespace",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "name",
"description": "name of the Scale",
"required": true,
"allowMultiple": false
}
],
"responseMessages": [
{
"code": 200,
"message": "OK",
"responseModel": "v1beta1.Scale"
}
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"consumes": [
"*/*"
]
},
{
"type": "v1beta1.Scale",
"method": "PUT",
"summary": "replace scale of the specified StatefulSet",
"nickname": "replaceNamespacedStatefulSetScale",
"parameters": [
{
"type": "string",
"paramType": "query",
"name": "pretty",
"description": "If 'true', then the output is pretty printed.",
"required": false,
"allowMultiple": false
},
{
"type": "v1beta1.Scale",
"paramType": "body",
"name": "body",
"description": "",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "namespace",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "name",
"description": "name of the Scale",
"required": true,
"allowMultiple": false
}
],
"responseMessages": [
{
"code": 200,
"message": "OK",
"responseModel": "v1beta1.Scale"
}
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"consumes": [
"*/*"
]
},
{
"type": "v1beta1.Scale",
"method": "PATCH",
"summary": "partially update scale of the specified StatefulSet",
"nickname": "patchNamespacedStatefulSetScale",
"parameters": [
{
"type": "string",
"paramType": "query",
"name": "pretty",
"description": "If 'true', then the output is pretty printed.",
"required": false,
"allowMultiple": false
},
{
"type": "v1.Patch",
"paramType": "body",
"name": "body",
"description": "",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "namespace",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "name",
"description": "name of the Scale",
"required": true,
"allowMultiple": false
}
],
"responseMessages": [
{
"code": 200,
"message": "OK",
"responseModel": "v1beta1.Scale"
}
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"consumes": [
"application/json-patch+json",
"application/merge-patch+json",
"application/strategic-merge-patch+json"
]
}
]
},
{
"path": "/apis/apps/v1beta1/namespaces/{namespace}/statefulsets/{name}/status",
"description": "API at /apis/apps/v1beta1",

View File

@ -4338,6 +4338,171 @@
}
]
},
{
"path": "/apis/apps/v1beta2/namespaces/{namespace}/statefulsets/{name}/scale",
"description": "API at /apis/apps/v1beta2",
"operations": [
{
"type": "v1beta2.Scale",
"method": "GET",
"summary": "read scale of the specified StatefulSet",
"nickname": "readNamespacedStatefulSetScale",
"parameters": [
{
"type": "string",
"paramType": "query",
"name": "pretty",
"description": "If 'true', then the output is pretty printed.",
"required": false,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "namespace",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "name",
"description": "name of the Scale",
"required": true,
"allowMultiple": false
}
],
"responseMessages": [
{
"code": 200,
"message": "OK",
"responseModel": "v1beta2.Scale"
}
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"consumes": [
"*/*"
]
},
{
"type": "v1beta2.Scale",
"method": "PUT",
"summary": "replace scale of the specified StatefulSet",
"nickname": "replaceNamespacedStatefulSetScale",
"parameters": [
{
"type": "string",
"paramType": "query",
"name": "pretty",
"description": "If 'true', then the output is pretty printed.",
"required": false,
"allowMultiple": false
},
{
"type": "v1beta2.Scale",
"paramType": "body",
"name": "body",
"description": "",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "namespace",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "name",
"description": "name of the Scale",
"required": true,
"allowMultiple": false
}
],
"responseMessages": [
{
"code": 200,
"message": "OK",
"responseModel": "v1beta2.Scale"
}
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"consumes": [
"*/*"
]
},
{
"type": "v1beta2.Scale",
"method": "PATCH",
"summary": "partially update scale of the specified StatefulSet",
"nickname": "patchNamespacedStatefulSetScale",
"parameters": [
{
"type": "string",
"paramType": "query",
"name": "pretty",
"description": "If 'true', then the output is pretty printed.",
"required": false,
"allowMultiple": false
},
{
"type": "v1.Patch",
"paramType": "body",
"name": "body",
"description": "",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "namespace",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "name",
"description": "name of the Scale",
"required": true,
"allowMultiple": false
}
],
"responseMessages": [
{
"code": 200,
"message": "OK",
"responseModel": "v1beta2.Scale"
}
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"consumes": [
"application/json-patch+json",
"application/merge-patch+json",
"application/strategic-merge-patch+json"
]
}
]
},
{
"path": "/apis/apps/v1beta2/namespaces/{namespace}/statefulsets/{name}/status",
"description": "API at /apis/apps/v1beta2",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -63,9 +63,10 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag
storage["deployments/scale"] = deploymentStorage.Scale
}
if apiResourceConfigSource.ResourceEnabled(version.WithResource("statefulsets")) {
statefulsetStorage, statefulsetStatusStorage := statefulsetstore.NewREST(restOptionsGetter)
storage["statefulsets"] = statefulsetStorage
storage["statefulsets/status"] = statefulsetStatusStorage
statefulSetStorage := statefulsetstore.NewStorage(restOptionsGetter)
storage["statefulsets"] = statefulSetStorage.StatefulSet
storage["statefulsets/status"] = statefulSetStorage.Status
storage["statefulsets/scale"] = statefulSetStorage.Scale
}
if apiResourceConfigSource.ResourceEnabled(version.WithResource("controllerrevisions")) {
historyStorage := controllerrevisionsstore.NewREST(restOptionsGetter)
@ -86,9 +87,10 @@ func (p RESTStorageProvider) v1beta2Storage(apiResourceConfigSource serverstorag
storage["deployments/scale"] = deploymentStorage.Scale
}
if apiResourceConfigSource.ResourceEnabled(version.WithResource("statefulsets")) {
statefulsetStorage, statefulsetStatusStorage := statefulsetstore.NewREST(restOptionsGetter)
storage["statefulsets"] = statefulsetStorage
storage["statefulsets/status"] = statefulsetStatusStorage
statefulSetStorage := statefulsetstore.NewStorage(restOptionsGetter)
storage["statefulsets"] = statefulSetStorage.StatefulSet
storage["statefulsets/status"] = statefulSetStorage.Status
storage["statefulsets/scale"] = statefulSetStorage.Scale
}
if apiResourceConfigSource.ResourceEnabled(version.WithResource("daemonsets")) {
daemonSetStorage, daemonSetStatusStorage := daemonsetstore.NewREST(restOptionsGetter)

View File

@ -12,6 +12,7 @@ go_library(
name = "go_default_library",
srcs = [
"doc.go",
"registry.go",
"strategy.go",
],
tags = ["automanaged"],
@ -20,8 +21,12 @@ go_library(
"//pkg/apis/apps:go_default_library",
"//pkg/apis/apps/validation:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",

View File

@ -0,0 +1,95 @@
/*
Copyright 2017 The Kubernetes Authors.
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 statefulset
import (
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/apps"
)
// Registry is an interface for things that know how to store StatefulSets.
type Registry interface {
ListStatefulSets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*apps.StatefulSetList, error)
WatchStatefulSets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error)
GetStatefulSet(ctx genericapirequest.Context, statefulSetID string, options *metav1.GetOptions) (*apps.StatefulSet, error)
CreateStatefulSet(ctx genericapirequest.Context, statefulSet *apps.StatefulSet) (*apps.StatefulSet, error)
UpdateStatefulSet(ctx genericapirequest.Context, statefulSet *apps.StatefulSet) (*apps.StatefulSet, error)
DeleteStatefulSet(ctx genericapirequest.Context, statefulSetID 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}
}
func (s *storage) ListStatefulSets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*apps.StatefulSetList, error) {
if options != nil && options.FieldSelector != nil && !options.FieldSelector.Empty() {
return nil, fmt.Errorf("field selector not supported yet")
}
obj, err := s.List(ctx, options)
if err != nil {
return nil, err
}
return obj.(*apps.StatefulSetList), err
}
func (s *storage) WatchStatefulSets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
return s.Watch(ctx, options)
}
func (s *storage) GetStatefulSet(ctx genericapirequest.Context, statefulSetID string, options *metav1.GetOptions) (*apps.StatefulSet, error) {
obj, err := s.Get(ctx, statefulSetID, options)
if err != nil {
return nil, errors.NewNotFound(apps.Resource("statefulsets/scale"), statefulSetID)
}
return obj.(*apps.StatefulSet), nil
}
func (s *storage) CreateStatefulSet(ctx genericapirequest.Context, statefulSet *apps.StatefulSet) (*apps.StatefulSet, error) {
obj, err := s.Create(ctx, statefulSet, false)
if err != nil {
return nil, err
}
return obj.(*apps.StatefulSet), nil
}
func (s *storage) UpdateStatefulSet(ctx genericapirequest.Context, statefulSet *apps.StatefulSet) (*apps.StatefulSet, error) {
obj, _, err := s.Update(ctx, statefulSet.Name, rest.DefaultUpdatedObjectInfo(statefulSet, api.Scheme))
if err != nil {
return nil, err
}
return obj.(*apps.StatefulSet), nil
}
func (s *storage) DeleteStatefulSet(ctx genericapirequest.Context, statefulSetID string) error {
_, _, err := s.Delete(ctx, statefulSetID, nil)
return err
}

View File

@ -16,10 +16,14 @@ go_test(
deps = [
"//pkg/api:go_default_library",
"//pkg/apis/apps:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//pkg/registry/registrytest:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
@ -34,8 +38,11 @@ go_library(
deps = [
"//pkg/api:go_default_library",
"//pkg/apis/apps:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//pkg/apis/extensions/validation:go_default_library",
"//pkg/registry/apps/statefulset:go_default_library",
"//pkg/registry/cachesize:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",

View File

@ -17,6 +17,9 @@ limitations under the License.
package storage
import (
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
@ -24,23 +27,43 @@ import (
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/kubernetes/pkg/api"
appsapi "k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/extensions"
extvalidation "k8s.io/kubernetes/pkg/apis/extensions/validation"
"k8s.io/kubernetes/pkg/registry/apps/statefulset"
"k8s.io/kubernetes/pkg/registry/cachesize"
)
// rest implements a RESTStorage for replication controllers against etcd
// StatefulSetStorage includes dummy storage for StatefulSets, and their Status and Scale subresource.
type StatefulSetStorage struct {
StatefulSet *REST
Status *StatusREST
Scale *ScaleREST
}
func NewStorage(optsGetter generic.RESTOptionsGetter) StatefulSetStorage {
statefulSetRest, statefulSetStatusRest := NewREST(optsGetter)
statefulSetRegistry := statefulset.NewRegistry(statefulSetRest)
return StatefulSetStorage{
StatefulSet: statefulSetRest,
Status: statefulSetStatusRest,
Scale: &ScaleREST{registry: statefulSetRegistry},
}
}
// rest implements a RESTStorage for statefulsets against etcd
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against replication controllers.
// NewREST returns a RESTStorage object that will work against statefulsets.
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST) {
store := &genericregistry.Store{
Copier: api.Scheme,
NewFunc: func() runtime.Object { return &appsapi.StatefulSet{} },
NewListFunc: func() runtime.Object { return &appsapi.StatefulSetList{} },
DefaultQualifiedResource: appsapi.Resource("statefulsets"),
NewFunc: func() runtime.Object { return &apps.StatefulSet{} },
NewListFunc: func() runtime.Object { return &apps.StatefulSetList{} },
DefaultQualifiedResource: apps.Resource("statefulsets"),
WatchCacheSize: cachesize.GetWatchCacheSizeByResource("statefulsets"),
CreateStrategy: statefulset.Strategy,
@ -71,7 +94,7 @@ type StatusREST struct {
}
func (r *StatusREST) New() runtime.Object {
return &appsapi.StatefulSet{}
return &apps.StatefulSet{}
}
// Get retrieves the object from the storage. It is required to support Patch.
@ -91,3 +114,88 @@ var _ rest.ShortNamesProvider = &REST{}
func (r *REST) ShortNames() []string {
return []string{"sts"}
}
type ScaleREST struct {
registry statefulset.Registry
}
// ScaleREST implements Patcher
var _ = rest.Patcher(&ScaleREST{})
// New creates a new Scale object
func (r *ScaleREST) New() runtime.Object {
return &extensions.Scale{}
}
func (r *ScaleREST) Get(ctx genericapirequest.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ss, err := r.registry.GetStatefulSet(ctx, name, options)
if err != nil {
return nil, err
}
scale, err := scaleFromStatefulSet(ss)
if err != nil {
return nil, errors.NewBadRequest(fmt.Sprintf("%v", err))
}
return scale, err
}
func (r *ScaleREST) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) {
ss, err := r.registry.GetStatefulSet(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
oldScale, err := scaleFromStatefulSet(ss)
if err != nil {
return nil, false, err
}
obj, err := objInfo.UpdatedObject(ctx, oldScale)
if err != nil {
return nil, false, err
}
if obj == nil {
return nil, false, errors.NewBadRequest(fmt.Sprintf("nil update passed to Scale"))
}
scale, ok := obj.(*extensions.Scale)
if !ok {
return nil, false, errors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj))
}
if errs := extvalidation.ValidateScale(scale); len(errs) > 0 {
return nil, false, errors.NewInvalid(extensions.Kind("Scale"), scale.Name, errs)
}
ss.Spec.Replicas = scale.Spec.Replicas
ss.ResourceVersion = scale.ResourceVersion
ss, err = r.registry.UpdateStatefulSet(ctx, ss)
if err != nil {
return nil, false, err
}
newScale, err := scaleFromStatefulSet(ss)
if err != nil {
return nil, false, errors.NewBadRequest(fmt.Sprintf("%v", err))
}
return newScale, false, err
}
// scaleFromStatefulSet returns a scale subresource for a statefulset.
func scaleFromStatefulSet(ss *apps.StatefulSet) (*extensions.Scale, error) {
return &extensions.Scale{
// TODO: Create a variant of ObjectMeta type that only contains the fields below.
ObjectMeta: metav1.ObjectMeta{
Name: ss.Name,
Namespace: ss.Namespace,
UID: ss.UID,
ResourceVersion: ss.ResourceVersion,
CreationTimestamp: ss.CreationTimestamp,
},
Spec: extensions.ScaleSpec{
Replicas: ss.Spec.Replicas,
},
Status: extensions.ScaleStatus{
Replicas: ss.Status.Replicas,
Selector: ss.Spec.Selector,
},
}, nil
}

View File

@ -19,24 +19,28 @@ package storage
import (
"testing"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/diff"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/registry/registrytest"
)
// TODO: allow for global factory override
func newStorage(t *testing.T) (*REST, *StatusREST, *etcdtesting.EtcdTestServer) {
func newStorage(t *testing.T) (StatefulSetStorage, *etcdtesting.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorage(t, apps.GroupName)
restOptions := generic.RESTOptions{StorageConfig: etcdStorage, Decorator: generic.UndecoratedStorage, DeleteCollectionWorkers: 1, ResourcePrefix: "statefulsets"}
statefulSetStorage, statusStorage := NewREST(restOptions)
return statefulSetStorage, statusStorage, server
storage := NewStorage(restOptions)
return storage, server
}
// createStatefulSet is a helper function that returns a StatefulSet with the updated resource version.
@ -83,11 +87,13 @@ func validNewStatefulSet() *apps.StatefulSet {
}
}
var validStatefulSet = *validNewStatefulSet()
func TestCreate(t *testing.T) {
storage, _, server := newStorage(t)
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := registrytest.New(t, storage.Store)
defer storage.StatefulSet.Store.DestroyFunc()
test := registrytest.New(t, storage.StatefulSet.Store)
ps := validNewStatefulSet()
ps.ObjectMeta = metav1.ObjectMeta{}
test.TestCreate(
@ -100,13 +106,13 @@ func TestCreate(t *testing.T) {
// TODO: Test updates to spec when we allow them.
func TestStatusUpdate(t *testing.T) {
storage, statusStorage, server := newStorage(t)
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
defer storage.StatefulSet.Store.DestroyFunc()
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/statefulsets/" + metav1.NamespaceDefault + "/foo"
validStatefulSet := validNewStatefulSet()
if err := storage.Storage.Create(ctx, key, validStatefulSet, nil, 0); err != nil {
if err := storage.StatefulSet.Storage.Create(ctx, key, validStatefulSet, nil, 0); err != nil {
t.Fatalf("unexpected error: %v", err)
}
update := apps.StatefulSet{
@ -119,10 +125,10 @@ func TestStatusUpdate(t *testing.T) {
},
}
if _, _, err := statusStorage.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update, api.Scheme)); err != nil {
if _, _, err := storage.Status.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update, api.Scheme)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
obj, err := storage.Get(ctx, "foo", &metav1.GetOptions{})
obj, err := storage.StatefulSet.Get(ctx, "foo", &metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -137,34 +143,34 @@ func TestStatusUpdate(t *testing.T) {
}
func TestGet(t *testing.T) {
storage, _, server := newStorage(t)
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := registrytest.New(t, storage.Store)
defer storage.StatefulSet.Store.DestroyFunc()
test := registrytest.New(t, storage.StatefulSet.Store)
test.TestGet(validNewStatefulSet())
}
func TestList(t *testing.T) {
storage, _, server := newStorage(t)
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := registrytest.New(t, storage.Store)
defer storage.StatefulSet.Store.DestroyFunc()
test := registrytest.New(t, storage.StatefulSet.Store)
test.TestList(validNewStatefulSet())
}
func TestDelete(t *testing.T) {
storage, _, server := newStorage(t)
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := registrytest.New(t, storage.Store)
defer storage.StatefulSet.Store.DestroyFunc()
test := registrytest.New(t, storage.StatefulSet.Store)
test.TestDelete(validNewStatefulSet())
}
func TestWatch(t *testing.T) {
storage, _, server := newStorage(t)
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := registrytest.New(t, storage.Store)
defer storage.StatefulSet.Store.DestroyFunc()
test := registrytest.New(t, storage.StatefulSet.Store)
test.TestWatch(
validNewStatefulSet(),
// matching labels
@ -189,11 +195,104 @@ func TestWatch(t *testing.T) {
}
func TestCategories(t *testing.T) {
storage, _, server := newStorage(t)
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
defer storage.StatefulSet.Store.DestroyFunc()
expected := []string{"all"}
registrytest.AssertCategories(t, storage, expected)
registrytest.AssertCategories(t, storage.StatefulSet, expected)
}
func TestShortNames(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.StatefulSet.Store.DestroyFunc()
expected := []string{"sts"}
registrytest.AssertShortNames(t, storage.StatefulSet, expected)
}
func TestScaleGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.StatefulSet.Store.DestroyFunc()
name := "foo"
var sts apps.StatefulSet
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/statefulsets/" + metav1.NamespaceDefault + "/" + name
if err := storage.StatefulSet.Storage.Create(ctx, key, &validStatefulSet, &sts, 0); err != nil {
t.Fatalf("error setting new statefulset (key: %s) %v: %v", key, validStatefulSet, err)
}
want := &extensions.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: metav1.NamespaceDefault,
UID: sts.UID,
ResourceVersion: sts.ResourceVersion,
CreationTimestamp: sts.CreationTimestamp,
},
Spec: extensions.ScaleSpec{
Replicas: validStatefulSet.Spec.Replicas,
},
Status: extensions.ScaleStatus{
Replicas: validStatefulSet.Status.Replicas,
Selector: validStatefulSet.Spec.Selector,
},
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
got := obj.(*extensions.Scale)
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
if !apiequality.Semantic.DeepEqual(got, want) {
t.Errorf("unexpected scale: %s", diff.ObjectDiff(got, want))
}
}
func TestScaleUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.StatefulSet.Store.DestroyFunc()
name := "foo"
var sts apps.StatefulSet
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/statefulsets/" + metav1.NamespaceDefault + "/" + name
if err := storage.StatefulSet.Storage.Create(ctx, key, &validStatefulSet, &sts, 0); err != nil {
t.Fatalf("error setting new statefulset (key: %s) %v: %v", key, validStatefulSet, err)
}
replicas := 12
update := extensions.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: metav1.NamespaceDefault,
},
Spec: extensions.ScaleSpec{
Replicas: int32(replicas),
},
}
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update, api.Scheme)); err != nil {
t.Fatalf("error updating scale %v: %v", update, err)
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
scale := obj.(*extensions.Scale)
if scale.Spec.Replicas != int32(replicas) {
t.Errorf("wrong replicas count expected: %d got: %d", replicas, scale.Spec.Replicas)
}
update.ResourceVersion = sts.ResourceVersion
update.Spec.Replicas = 15
if _, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update, api.Scheme)); err != nil && !errors.IsConflict(err) {
t.Fatalf("unexpected error, expecting an update conflict but got %v", err)
}
}
// TODO: Test generation number.

View File

@ -843,6 +843,43 @@ var _ = SIGDescribe("StatefulSet", func() {
return nil
}, framework.StatefulPodTimeout, 2*time.Second).Should(BeNil())
})
It("should have a working scale subresource", func() {
By("Creating statefulset " + ssName + " in namespace " + ns)
ss := framework.NewStatefulSet(ssName, ns, headlessSvcName, 1, nil, nil, labels)
sst := framework.NewStatefulSetTester(c)
sst.SetHttpProbe(ss)
ss, err := c.AppsV1beta1().StatefulSets(ns).Create(ss)
Expect(err).NotTo(HaveOccurred())
sst.WaitForRunningAndReady(*ss.Spec.Replicas, ss)
ss = sst.WaitForStatus(ss)
By("getting scale subresource")
scale := framework.NewStatefulSetScale(ss)
scaleResult := &apps.Scale{}
err = c.AppsV1beta1().RESTClient().Get().AbsPath("/apis/apps/v1beta1").Namespace(ns).Resource("statefulsets").Name(ssName).SubResource("scale").Do().Into(scale)
if err != nil {
framework.Failf("Failed to get scale subresource: %v", err)
}
Expect(scale.Spec.Replicas).To(Equal(int32(1)))
Expect(scale.Status.Replicas).To(Equal(int32(1)))
By("updating a scale subresource")
scale.ResourceVersion = "" //unconditionally update to 2 replicas
scale.Spec.Replicas = 2
err = c.AppsV1beta1().RESTClient().Put().AbsPath("/apis/apps/v1beta1").Namespace(ns).Resource("statefulsets").Name(ssName).SubResource("scale").Body(scale).Do().Into(scaleResult)
if err != nil {
framework.Failf("Failed to put scale subresource: %v", err)
}
Expect(scaleResult.Spec.Replicas).To(Equal(int32(2)))
By("verifying the statefulset Spec.Replicas was modified")
ss, err = c.AppsV1beta1().StatefulSets(ns).Get(ssName, metav1.GetOptions{})
if err != nil {
framework.Failf("Failed to get statefulset resource: %v", err)
}
Expect(*(ss.Spec.Replicas)).To(Equal(int32(2)))
})
})
framework.KubeDescribe("Deploy clustered applications [Feature:StatefulSet] [Slow]", func() {

View File

@ -101,6 +101,7 @@ go_library(
"//vendor/google.golang.org/api/compute/v1:go_default_library",
"//vendor/google.golang.org/api/googleapi:go_default_library",
"//vendor/k8s.io/api/apps/v1beta1:go_default_library",
"//vendor/k8s.io/api/apps/v1beta2:go_default_library",
"//vendor/k8s.io/api/authorization/v1beta1:go_default_library",
"//vendor/k8s.io/api/batch/v1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",

View File

@ -29,6 +29,7 @@ import (
. "github.com/onsi/gomega"
apps "k8s.io/api/apps/v1beta1"
appsV1beta2 "k8s.io/api/apps/v1beta2"
"k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
@ -224,7 +225,7 @@ func (s *StatefulSetTester) Scale(ss *apps.StatefulSet, count int32) error {
// UpdateReplicas updates the replicas of ss to count.
func (s *StatefulSetTester) UpdateReplicas(ss *apps.StatefulSet, count int32) {
s.update(ss.Namespace, ss.Name, func(ss *apps.StatefulSet) { ss.Spec.Replicas = &count })
s.update(ss.Namespace, ss.Name, func(ss *apps.StatefulSet) { *(ss.Spec.Replicas) = count })
}
// Restart scales ss to 0 and then back to its previous number of replicas.
@ -812,6 +813,23 @@ func NewStatefulSet(name, ns, governingSvcName string, replicas int32, statefulP
}
}
// NewStatefulSetScale creates a new StatefulSet scale subresource and returns it
func NewStatefulSetScale(ss *apps.StatefulSet) *appsV1beta2.Scale {
return &appsV1beta2.Scale{
// TODO: Create a variant of ObjectMeta type that only contains the fields below.
ObjectMeta: metav1.ObjectMeta{
Name: ss.Name,
Namespace: ss.Namespace,
},
Spec: appsV1beta2.ScaleSpec{
Replicas: *(ss.Spec.Replicas),
},
Status: appsV1beta2.ScaleStatus{
Replicas: ss.Status.Replicas,
},
}
}
var statefulPodRegex = regexp.MustCompile("(.*)-([0-9]+)$")
func getStatefulPodOrdinal(pod *v1.Pod) int {