mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 13:37:30 +00:00
Merge pull request #14034 from jszczepkowski/hpa-unittest
Unittests for horizontal pod autoscaler controller.
This commit is contained in:
commit
81d3bd9a36
@ -77,6 +77,5 @@ func (c *FakeServices) Watch(label labels.Selector, field fields.Selector, resou
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *FakeServices) ProxyGet(name, path string, params map[string]string) unversioned.ResponseWrapper {
|
func (c *FakeServices) ProxyGet(name, path string, params map[string]string) unversioned.ResponseWrapper {
|
||||||
c.Fake.Invokes(NewProxyGetAction("services", c.Namespace, name, path, params), nil)
|
return c.Fake.InvokesProxy(NewProxyGetAction("services", c.Namespace, name, path, params))
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/api/errors"
|
"k8s.io/kubernetes/pkg/api/errors"
|
||||||
"k8s.io/kubernetes/pkg/api/meta"
|
"k8s.io/kubernetes/pkg/api/meta"
|
||||||
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||||
"k8s.io/kubernetes/pkg/runtime"
|
"k8s.io/kubernetes/pkg/runtime"
|
||||||
"k8s.io/kubernetes/pkg/util/yaml"
|
"k8s.io/kubernetes/pkg/util/yaml"
|
||||||
"k8s.io/kubernetes/pkg/watch"
|
"k8s.io/kubernetes/pkg/watch"
|
||||||
@ -260,7 +261,7 @@ func (r *SimpleReactor) React(action Action) (bool, runtime.Object, error) {
|
|||||||
return r.Reaction(action)
|
return r.Reaction(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimpleWatchReactor is a Reactor. Each reaction function is attached to a given verb,resource tuple. "*" in either field matches everything for that value.
|
// SimpleWatchReactor is a WatchReactor. Each reaction function is attached to a given resource. "*" matches everything for that value.
|
||||||
// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions
|
// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions
|
||||||
type SimpleWatchReactor struct {
|
type SimpleWatchReactor struct {
|
||||||
Resource string
|
Resource string
|
||||||
@ -280,3 +281,24 @@ func (r *SimpleWatchReactor) Handles(action Action) bool {
|
|||||||
func (r *SimpleWatchReactor) React(action Action) (bool, watch.Interface, error) {
|
func (r *SimpleWatchReactor) React(action Action) (bool, watch.Interface, error) {
|
||||||
return r.Reaction(action)
|
return r.Reaction(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SimpleProxyReactor is a ProxyReactor. Each reaction function is attached to a given resource. "*" matches everything for that value.
|
||||||
|
// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions.
|
||||||
|
type SimpleProxyReactor struct {
|
||||||
|
Resource string
|
||||||
|
|
||||||
|
Reaction ProxyReactionFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SimpleProxyReactor) Handles(action Action) bool {
|
||||||
|
resourceCovers := r.Resource == "*" || r.Resource == action.GetResource()
|
||||||
|
if !resourceCovers {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SimpleProxyReactor) React(action Action) (bool, client.ResponseWrapper, error) {
|
||||||
|
return r.Reaction(action)
|
||||||
|
}
|
||||||
|
@ -54,6 +54,8 @@ type Fake struct {
|
|||||||
ReactionChain []Reactor
|
ReactionChain []Reactor
|
||||||
// WatchReactionChain is the list of watch reactors that will be attempted for every request in the order they are tried
|
// WatchReactionChain is the list of watch reactors that will be attempted for every request in the order they are tried
|
||||||
WatchReactionChain []WatchReactor
|
WatchReactionChain []WatchReactor
|
||||||
|
// ProxyReactionChain is the list of proxy reactors that will be attempted for every request in the order they are tried
|
||||||
|
ProxyReactionChain []ProxyReactor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactor is an interface to allow the composition of reaction functions.
|
// Reactor is an interface to allow the composition of reaction functions.
|
||||||
@ -72,6 +74,14 @@ type WatchReactor interface {
|
|||||||
React(action Action) (handled bool, ret watch.Interface, err error)
|
React(action Action) (handled bool, ret watch.Interface, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyReactor is an interface to allow the composition of proxy get functions.
|
||||||
|
type ProxyReactor interface {
|
||||||
|
// Handles indicates whether or not this Reactor deals with a given action
|
||||||
|
Handles(action Action) bool
|
||||||
|
// React handles a watch action and returns results. It may choose to delegate by indicated handled=false
|
||||||
|
React(action Action) (handled bool, ret client.ResponseWrapper, err error)
|
||||||
|
}
|
||||||
|
|
||||||
// ReactionFunc is a function that returns an object or error for a given Action. If "handled" is false,
|
// ReactionFunc is a function that returns an object or error for a given Action. If "handled" is false,
|
||||||
// then the test client will continue ignore the results and continue to the next ReactionFunc
|
// then the test client will continue ignore the results and continue to the next ReactionFunc
|
||||||
type ReactionFunc func(action Action) (handled bool, ret runtime.Object, err error)
|
type ReactionFunc func(action Action) (handled bool, ret runtime.Object, err error)
|
||||||
@ -80,6 +90,10 @@ type ReactionFunc func(action Action) (handled bool, ret runtime.Object, err err
|
|||||||
// then the test client will continue ignore the results and continue to the next ReactionFunc
|
// then the test client will continue ignore the results and continue to the next ReactionFunc
|
||||||
type WatchReactionFunc func(action Action) (handled bool, ret watch.Interface, err error)
|
type WatchReactionFunc func(action Action) (handled bool, ret watch.Interface, err error)
|
||||||
|
|
||||||
|
// ProxyReactionFunc is a function that returns a ResponseWrapper interface for a given Action. If "handled" is false,
|
||||||
|
// then the test client will continue ignore the results and continue to the next ProxyReactionFunc
|
||||||
|
type ProxyReactionFunc func(action Action) (handled bool, ret client.ResponseWrapper, err error)
|
||||||
|
|
||||||
// AddReactor appends a reactor to the end of the chain
|
// AddReactor appends a reactor to the end of the chain
|
||||||
func (c *Fake) AddReactor(verb, resource string, reaction ReactionFunc) {
|
func (c *Fake) AddReactor(verb, resource string, reaction ReactionFunc) {
|
||||||
c.ReactionChain = append(c.ReactionChain, &SimpleReactor{verb, resource, reaction})
|
c.ReactionChain = append(c.ReactionChain, &SimpleReactor{verb, resource, reaction})
|
||||||
@ -97,6 +111,11 @@ func (c *Fake) AddWatchReactor(resource string, reaction WatchReactionFunc) {
|
|||||||
c.WatchReactionChain = append(c.WatchReactionChain, &SimpleWatchReactor{resource, reaction})
|
c.WatchReactionChain = append(c.WatchReactionChain, &SimpleWatchReactor{resource, reaction})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddProxyReactor appends a reactor to the end of the chain
|
||||||
|
func (c *Fake) AddProxyReactor(resource string, reaction ProxyReactionFunc) {
|
||||||
|
c.ProxyReactionChain = append(c.ProxyReactionChain, &SimpleProxyReactor{resource, reaction})
|
||||||
|
}
|
||||||
|
|
||||||
// Invokes records the provided Action and then invokes the ReactFn (if provided).
|
// Invokes records the provided Action and then invokes the ReactFn (if provided).
|
||||||
// defaultReturnObj is expected to be of the same type a normal call would return.
|
// defaultReturnObj is expected to be of the same type a normal call would return.
|
||||||
func (c *Fake) Invokes(action Action, defaultReturnObj runtime.Object) (runtime.Object, error) {
|
func (c *Fake) Invokes(action Action, defaultReturnObj runtime.Object) (runtime.Object, error) {
|
||||||
@ -142,6 +161,28 @@ func (c *Fake) InvokesWatch(action Action) (watch.Interface, error) {
|
|||||||
return nil, fmt.Errorf("unhandled watch: %#v", action)
|
return nil, fmt.Errorf("unhandled watch: %#v", action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvokesProxy records the provided Action and then invokes the ReactFn (if provided).
|
||||||
|
func (c *Fake) InvokesProxy(action Action) client.ResponseWrapper {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
c.actions = append(c.actions, action)
|
||||||
|
for _, reactor := range c.ProxyReactionChain {
|
||||||
|
if !reactor.Handles(action) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
handled, ret, err := reactor.React(action)
|
||||||
|
if !handled || err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ClearActions clears the history of actions called on the fake client
|
// ClearActions clears the history of actions called on the fake client
|
||||||
func (c *Fake) ClearActions() {
|
func (c *Fake) ClearActions() {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
|
@ -79,8 +79,9 @@ func (a *HorizontalController) reconcileAutoscaler(hpa experimental.HorizontalPo
|
|||||||
return fmt.Errorf("failed to query scale subresource for %s: %v", reference, err)
|
return fmt.Errorf("failed to query scale subresource for %s: %v", reference, err)
|
||||||
}
|
}
|
||||||
currentReplicas := scale.Status.Replicas
|
currentReplicas := scale.Status.Replicas
|
||||||
currentConsumption, err := a.metricsClient.ResourceConsumption(hpa.Spec.ScaleRef.Namespace).Get(hpa.Spec.Target.Resource,
|
currentConsumption, err := a.metricsClient.
|
||||||
scale.Status.Selector)
|
ResourceConsumption(hpa.Spec.ScaleRef.Namespace).
|
||||||
|
Get(hpa.Spec.Target.Resource, scale.Status.Selector)
|
||||||
|
|
||||||
// TODO: what to do on partial errors (like metrics obtained for 75% of pods).
|
// TODO: what to do on partial errors (like metrics obtained for 75% of pods).
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -17,111 +17,73 @@ limitations under the License.
|
|||||||
package podautoscaler
|
package podautoscaler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"io"
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
_ "k8s.io/kubernetes/pkg/api/latest"
|
_ "k8s.io/kubernetes/pkg/api/latest"
|
||||||
"k8s.io/kubernetes/pkg/api/resource"
|
"k8s.io/kubernetes/pkg/api/resource"
|
||||||
"k8s.io/kubernetes/pkg/api/testapi"
|
|
||||||
"k8s.io/kubernetes/pkg/apis/experimental"
|
"k8s.io/kubernetes/pkg/apis/experimental"
|
||||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
|
||||||
"k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
|
"k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
|
||||||
"k8s.io/kubernetes/pkg/runtime"
|
"k8s.io/kubernetes/pkg/runtime"
|
||||||
"k8s.io/kubernetes/pkg/util"
|
|
||||||
|
|
||||||
"github.com/golang/glog"
|
heapster "k8s.io/heapster/api/v1/types"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func (w fakeResponseWrapper) DoRaw() ([]byte, error) {
|
||||||
namespace = api.NamespaceDefault
|
return w.raw, nil
|
||||||
rcName = "app-rc"
|
|
||||||
podNameLabel = "app"
|
|
||||||
hpaName = "foo"
|
|
||||||
|
|
||||||
hpaListHandler = "HpaList"
|
|
||||||
scaleHandler = "Scale"
|
|
||||||
updateHpaHandler = "HpaUpdate"
|
|
||||||
eventHandler = "Event"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serverResponse struct {
|
|
||||||
statusCode int
|
|
||||||
obj interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeMetricsClient struct {
|
func (w fakeResponseWrapper) Stream() (io.ReadCloser, error) {
|
||||||
consumption metrics.ResourceConsumptionClient
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeResourceConsumptionClient struct {
|
func newFakeResponseWrapper(raw []byte) fakeResponseWrapper {
|
||||||
metrics map[api.ResourceName]experimental.ResourceConsumption
|
return fakeResponseWrapper{raw: raw}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeMetricsClient) ResourceConsumption(namespace string) metrics.ResourceConsumptionClient {
|
type fakeResponseWrapper struct {
|
||||||
return f.consumption
|
raw []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeResourceConsumptionClient) Get(resource api.ResourceName, selector map[string]string) (*experimental.ResourceConsumption, error) {
|
type testCase struct {
|
||||||
consumption, found := f.metrics[resource]
|
minCount int
|
||||||
if !found {
|
maxCount int
|
||||||
return nil, fmt.Errorf("resource not found: %v", resource)
|
initialReplicas int
|
||||||
}
|
desiredReplicas int
|
||||||
return &consumption, nil
|
targetResource api.ResourceName
|
||||||
|
targetLevel resource.Quantity
|
||||||
|
reportedLevels []uint64
|
||||||
|
scaleUpdated bool
|
||||||
|
eventCreated bool
|
||||||
|
verifyEvents bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTestServer(t *testing.T, responses map[string]*serverResponse) (*httptest.Server, map[string]*util.FakeHandler) {
|
func (tc *testCase) prepareTestClient(t *testing.T) *testclient.Fake {
|
||||||
|
namespace := "test-namespace"
|
||||||
|
hpaName := "test-hpa"
|
||||||
|
rcName := "test-rc"
|
||||||
|
podNamePrefix := "test-pod"
|
||||||
|
|
||||||
handlers := map[string]*util.FakeHandler{}
|
tc.scaleUpdated = false
|
||||||
mux := http.NewServeMux()
|
tc.eventCreated = false
|
||||||
|
|
||||||
mkHandler := func(url string, response serverResponse) *util.FakeHandler {
|
fakeClient := &testclient.Fake{}
|
||||||
handler := util.FakeHandler{
|
fakeClient.AddReactor("list", "horizontalpodautoscalers", func(action testclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
StatusCode: response.statusCode,
|
obj := &experimental.HorizontalPodAutoscalerList{
|
||||||
ResponseBody: runtime.EncodeOrDie(testapi.Experimental.Codec(), response.obj.(runtime.Object)),
|
|
||||||
}
|
|
||||||
mux.Handle(url, &handler)
|
|
||||||
glog.Infof("Will handle %s", url)
|
|
||||||
return &handler
|
|
||||||
}
|
|
||||||
|
|
||||||
if responses[hpaListHandler] != nil {
|
|
||||||
handlers[hpaListHandler] = mkHandler("/apis/experimental/v1/horizontalpodautoscalers", *responses[hpaListHandler])
|
|
||||||
}
|
|
||||||
|
|
||||||
if responses[scaleHandler] != nil {
|
|
||||||
handlers[scaleHandler] = mkHandler(
|
|
||||||
fmt.Sprintf("/apis/experimental/v1/namespaces/%s/replicationcontrollers/%s/scale", namespace, rcName), *responses[scaleHandler])
|
|
||||||
}
|
|
||||||
|
|
||||||
if responses[updateHpaHandler] != nil {
|
|
||||||
handlers[updateHpaHandler] = mkHandler(fmt.Sprintf("/apis/experimental/v1/namespaces/%s/horizontalpodautoscalers/%s", namespace, hpaName),
|
|
||||||
*responses[updateHpaHandler])
|
|
||||||
}
|
|
||||||
|
|
||||||
if responses[eventHandler] != nil {
|
|
||||||
handlers[eventHandler] = mkHandler(fmt.Sprintf("/api/v1/namespaces/%s/events", namespace),
|
|
||||||
*responses[eventHandler])
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
t.Errorf("unexpected request: %v", req.RequestURI)
|
|
||||||
res.WriteHeader(http.StatusNotFound)
|
|
||||||
})
|
|
||||||
return httptest.NewServer(mux), handlers
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncEndpointsItemsPreserveNoSelector(t *testing.T) {
|
|
||||||
hpaResponse := serverResponse{http.StatusOK, &experimental.HorizontalPodAutoscalerList{
|
|
||||||
Items: []experimental.HorizontalPodAutoscaler{
|
Items: []experimental.HorizontalPodAutoscaler{
|
||||||
{
|
{
|
||||||
ObjectMeta: api.ObjectMeta{
|
ObjectMeta: api.ObjectMeta{
|
||||||
Name: hpaName,
|
Name: hpaName,
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
SelfLink: "/apis/experimental/v1/namespaces/" + namespace + "/horizontalpodautoscalers/" + hpaName,
|
SelfLink: "experimental/v1/namespaces/" + namespace + "/horizontalpodautoscalers/" + hpaName,
|
||||||
},
|
},
|
||||||
Spec: experimental.HorizontalPodAutoscalerSpec{
|
Spec: experimental.HorizontalPodAutoscalerSpec{
|
||||||
ScaleRef: &experimental.SubresourceReference{
|
ScaleRef: &experimental.SubresourceReference{
|
||||||
@ -130,82 +92,270 @@ func TestSyncEndpointsItemsPreserveNoSelector(t *testing.T) {
|
|||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Subresource: "scale",
|
Subresource: "scale",
|
||||||
},
|
},
|
||||||
MinCount: 1,
|
MinCount: tc.minCount,
|
||||||
MaxCount: 5,
|
MaxCount: tc.maxCount,
|
||||||
Target: experimental.ResourceConsumption{Resource: api.ResourceCPU, Quantity: resource.MustParse("0.3")},
|
Target: experimental.ResourceConsumption{Resource: tc.targetResource, Quantity: tc.targetLevel},
|
||||||
},
|
},
|
||||||
}}}}
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return true, obj, nil
|
||||||
|
})
|
||||||
|
|
||||||
scaleResponse := serverResponse{http.StatusOK, &experimental.Scale{
|
fakeClient.AddReactor("get", "replicationController", func(action testclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
obj := &experimental.Scale{
|
||||||
ObjectMeta: api.ObjectMeta{
|
ObjectMeta: api.ObjectMeta{
|
||||||
Name: rcName,
|
Name: rcName,
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
},
|
},
|
||||||
Spec: experimental.ScaleSpec{
|
Spec: experimental.ScaleSpec{
|
||||||
Replicas: 1,
|
Replicas: tc.initialReplicas,
|
||||||
},
|
},
|
||||||
Status: experimental.ScaleStatus{
|
Status: experimental.ScaleStatus{
|
||||||
Replicas: 1,
|
Replicas: tc.initialReplicas,
|
||||||
Selector: map[string]string{"name": podNameLabel},
|
Selector: map[string]string{"name": podNamePrefix},
|
||||||
},
|
},
|
||||||
}}
|
|
||||||
|
|
||||||
status := experimental.HorizontalPodAutoscalerStatus{
|
|
||||||
CurrentReplicas: 1,
|
|
||||||
DesiredReplicas: 3,
|
|
||||||
}
|
}
|
||||||
updateHpaResponse := serverResponse{http.StatusOK, &experimental.HorizontalPodAutoscaler{
|
return true, obj, nil
|
||||||
ObjectMeta: api.ObjectMeta{
|
|
||||||
Name: hpaName,
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
Spec: experimental.HorizontalPodAutoscalerSpec{
|
|
||||||
ScaleRef: &experimental.SubresourceReference{
|
|
||||||
Kind: "replicationController",
|
|
||||||
Name: rcName,
|
|
||||||
Namespace: namespace,
|
|
||||||
Subresource: "scale",
|
|
||||||
},
|
|
||||||
MinCount: 1,
|
|
||||||
MaxCount: 5,
|
|
||||||
Target: experimental.ResourceConsumption{Resource: api.ResourceCPU, Quantity: resource.MustParse("0.3")},
|
|
||||||
},
|
|
||||||
Status: &status,
|
|
||||||
}}
|
|
||||||
|
|
||||||
eventResponse := serverResponse{http.StatusOK, &api.Event{}}
|
|
||||||
|
|
||||||
testServer, handlers := makeTestServer(t,
|
|
||||||
map[string]*serverResponse{
|
|
||||||
hpaListHandler: &hpaResponse,
|
|
||||||
scaleHandler: &scaleResponse,
|
|
||||||
updateHpaHandler: &updateHpaResponse,
|
|
||||||
eventHandler: &eventResponse,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defer testServer.Close()
|
fakeClient.AddReactor("list", "pods", func(action testclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
kubeClient := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Experimental.Version()})
|
obj := &api.PodList{}
|
||||||
fakeRC := fakeResourceConsumptionClient{metrics: map[api.ResourceName]experimental.ResourceConsumption{
|
for i := 0; i < tc.initialReplicas; i++ {
|
||||||
api.ResourceCPU: {Resource: api.ResourceCPU, Quantity: resource.MustParse("650m")},
|
podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
|
||||||
}}
|
pod := api.Pod{
|
||||||
fake := fakeMetricsClient{consumption: &fakeRC}
|
Status: api.PodStatus{
|
||||||
|
Phase: api.PodRunning,
|
||||||
|
},
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: podName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"name": podNamePrefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
obj.Items = append(obj.Items, pod)
|
||||||
|
}
|
||||||
|
return true, obj, nil
|
||||||
|
})
|
||||||
|
|
||||||
hpaController := NewHorizontalController(kubeClient, &fake)
|
fakeClient.AddProxyReactor("services", func(action testclient.Action) (handled bool, ret client.ResponseWrapper, err error) {
|
||||||
|
timestamp := time.Now()
|
||||||
|
metrics := heapster.MetricResultList{}
|
||||||
|
for _, level := range tc.reportedLevels {
|
||||||
|
metric := heapster.MetricResult{
|
||||||
|
Metrics: []heapster.MetricPoint{{timestamp, level}},
|
||||||
|
LatestTimestamp: timestamp,
|
||||||
|
}
|
||||||
|
metrics.Items = append(metrics.Items, metric)
|
||||||
|
}
|
||||||
|
heapsterRawMemResponse, _ := json.Marshal(&metrics)
|
||||||
|
return true, newFakeResponseWrapper(heapsterRawMemResponse), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
fakeClient.AddReactor("update", "replicationController", func(action testclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
obj := action.(testclient.UpdateAction).GetObject().(*experimental.Scale)
|
||||||
|
replicas := action.(testclient.UpdateAction).GetObject().(*experimental.Scale).Spec.Replicas
|
||||||
|
assert.Equal(t, tc.desiredReplicas, replicas)
|
||||||
|
tc.scaleUpdated = true
|
||||||
|
return true, obj, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
fakeClient.AddReactor("update", "horizontalpodautoscalers", func(action testclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
obj := action.(testclient.UpdateAction).GetObject().(*experimental.HorizontalPodAutoscaler)
|
||||||
|
assert.Equal(t, namespace, obj.Namespace)
|
||||||
|
assert.Equal(t, hpaName, obj.Name)
|
||||||
|
assert.Equal(t, tc.desiredReplicas, obj.Status.DesiredReplicas)
|
||||||
|
return true, obj, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
fakeClient.AddReactor("*", "events", func(action testclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
obj := action.(testclient.CreateAction).GetObject().(*api.Event)
|
||||||
|
if tc.verifyEvents {
|
||||||
|
assert.Equal(t, "SuccessfulRescale", obj.Reason)
|
||||||
|
assert.Equal(t, fmt.Sprintf("New size: %d", tc.desiredReplicas), obj.Message)
|
||||||
|
}
|
||||||
|
tc.eventCreated = true
|
||||||
|
return true, obj, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return fakeClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *testCase) verifyResults(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.initialReplicas != tc.desiredReplicas, tc.scaleUpdated)
|
||||||
|
if tc.verifyEvents {
|
||||||
|
assert.Equal(t, tc.initialReplicas != tc.desiredReplicas, tc.eventCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *testCase) runTest(t *testing.T) {
|
||||||
|
testClient := tc.prepareTestClient(t)
|
||||||
|
hpaController := NewHorizontalController(testClient, metrics.NewHeapsterMetricsClient(testClient))
|
||||||
err := hpaController.reconcileAutoscalers()
|
err := hpaController.reconcileAutoscalers()
|
||||||
if err != nil {
|
assert.Equal(t, nil, err)
|
||||||
t.Fatal("Failed to reconcile: %v", err)
|
if tc.verifyEvents {
|
||||||
|
// We need to wait for events to be broadcasted (sleep for longer than record.sleepDuration).
|
||||||
|
time.Sleep(12 * time.Second)
|
||||||
}
|
}
|
||||||
for _, h := range handlers {
|
tc.verifyResults(t)
|
||||||
h.ValidateRequestCount(t, 1)
|
|
||||||
}
|
}
|
||||||
obj, err := kubeClient.Codec.Decode([]byte(handlers[updateHpaHandler].RequestBody))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Failed to decode: %v %v", err)
|
|
||||||
}
|
|
||||||
hpa, _ := obj.(*experimental.HorizontalPodAutoscaler)
|
|
||||||
|
|
||||||
assert.Equal(t, 3, hpa.Status.DesiredReplicas)
|
func TestCPU(t *testing.T) {
|
||||||
assert.Equal(t, int64(650), hpa.Status.CurrentConsumption.Quantity.MilliValue())
|
tc := testCase{
|
||||||
assert.NotNil(t, hpa.Status.LastScaleTimestamp)
|
minCount: 1,
|
||||||
|
maxCount: 5,
|
||||||
|
initialReplicas: 1,
|
||||||
|
desiredReplicas: 2,
|
||||||
|
targetResource: api.ResourceCPU,
|
||||||
|
targetLevel: resource.MustParse("0.1"),
|
||||||
|
reportedLevels: []uint64{200},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemory(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 5,
|
||||||
|
initialReplicas: 1,
|
||||||
|
desiredReplicas: 2,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("1k"),
|
||||||
|
reportedLevels: []uint64{2000},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScaleUp(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 2,
|
||||||
|
maxCount: 6,
|
||||||
|
initialReplicas: 3,
|
||||||
|
desiredReplicas: 5,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("3k"),
|
||||||
|
reportedLevels: []uint64{3000, 5000, 7000},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScaleDown(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 2,
|
||||||
|
maxCount: 6,
|
||||||
|
initialReplicas: 5,
|
||||||
|
desiredReplicas: 3,
|
||||||
|
targetResource: api.ResourceCPU,
|
||||||
|
targetLevel: resource.MustParse("0.5"),
|
||||||
|
reportedLevels: []uint64{100, 300, 500, 250, 250},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTolerance(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 5,
|
||||||
|
initialReplicas: 3,
|
||||||
|
desiredReplicas: 3,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("1k"),
|
||||||
|
reportedLevels: []uint64{1010, 1030, 1020},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinCount(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 2,
|
||||||
|
maxCount: 5,
|
||||||
|
initialReplicas: 3,
|
||||||
|
desiredReplicas: 2,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("1k"),
|
||||||
|
reportedLevels: []uint64{10, 95, 10},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxCount(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 2,
|
||||||
|
maxCount: 5,
|
||||||
|
initialReplicas: 3,
|
||||||
|
desiredReplicas: 5,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("1k"),
|
||||||
|
reportedLevels: []uint64{8000, 9500, 1000},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuperfluousMetrics(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 2,
|
||||||
|
maxCount: 6,
|
||||||
|
initialReplicas: 4,
|
||||||
|
desiredReplicas: 4,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("1k"),
|
||||||
|
reportedLevels: []uint64{4000, 9500, 3000, 7000, 3200, 2000},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingMetrics(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 2,
|
||||||
|
maxCount: 6,
|
||||||
|
initialReplicas: 4,
|
||||||
|
desiredReplicas: 4,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("1k"),
|
||||||
|
reportedLevels: []uint64{400, 95},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyMetrics(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 2,
|
||||||
|
maxCount: 6,
|
||||||
|
initialReplicas: 4,
|
||||||
|
desiredReplicas: 4,
|
||||||
|
targetResource: api.ResourceMemory,
|
||||||
|
targetLevel: resource.MustParse("1k"),
|
||||||
|
reportedLevels: []uint64{},
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventCreated(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 5,
|
||||||
|
initialReplicas: 1,
|
||||||
|
desiredReplicas: 2,
|
||||||
|
targetResource: api.ResourceCPU,
|
||||||
|
targetLevel: resource.MustParse("0.1"),
|
||||||
|
reportedLevels: []uint64{200},
|
||||||
|
verifyEvents: true,
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventNotCreated(t *testing.T) {
|
||||||
|
tc := testCase{
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 5,
|
||||||
|
initialReplicas: 2,
|
||||||
|
desiredReplicas: 2,
|
||||||
|
targetResource: api.ResourceCPU,
|
||||||
|
targetLevel: resource.MustParse("0.2"),
|
||||||
|
reportedLevels: []uint64{200, 200},
|
||||||
|
verifyEvents: true,
|
||||||
|
}
|
||||||
|
tc.runTest(t)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user