controller: unit tests for overlapping and recreate deployments
This commit is contained in:
parent
e827393fa2
commit
df7a655d30
@ -73,6 +73,8 @@ type DeploymentController struct {
|
|||||||
|
|
||||||
// To allow injection of syncDeployment for testing.
|
// To allow injection of syncDeployment for testing.
|
||||||
syncHandler func(dKey string) error
|
syncHandler func(dKey string) error
|
||||||
|
// used for unit testing
|
||||||
|
enqueueDeployment func(deployment *extensions.Deployment)
|
||||||
|
|
||||||
// A store of deployments, populated by the dController
|
// A store of deployments, populated by the dController
|
||||||
dLister *cache.StoreToDeploymentLister
|
dLister *cache.StoreToDeploymentLister
|
||||||
@ -134,6 +136,8 @@ func NewDeploymentController(dInformer informers.DeploymentInformer, rsInformer
|
|||||||
})
|
})
|
||||||
|
|
||||||
dc.syncHandler = dc.syncDeployment
|
dc.syncHandler = dc.syncDeployment
|
||||||
|
dc.enqueueDeployment = dc.enqueue
|
||||||
|
|
||||||
dc.dLister = dInformer.Lister()
|
dc.dLister = dInformer.Lister()
|
||||||
dc.rsLister = rsInformer.Lister()
|
dc.rsLister = rsInformer.Lister()
|
||||||
dc.podLister = podInformer.Lister()
|
dc.podLister = podInformer.Lister()
|
||||||
@ -343,7 +347,7 @@ func (dc *DeploymentController) deletePod(obj interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *DeploymentController) enqueueDeployment(deployment *extensions.Deployment) {
|
func (dc *DeploymentController) enqueue(deployment *extensions.Deployment) {
|
||||||
key, err := controller.KeyFunc(deployment)
|
key, err := controller.KeyFunc(deployment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", deployment, err))
|
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", deployment, err))
|
||||||
|
@ -19,6 +19,7 @@ package deployment
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/api/v1"
|
"k8s.io/kubernetes/pkg/api/v1"
|
||||||
"k8s.io/kubernetes/pkg/apimachinery/registered"
|
"k8s.io/kubernetes/pkg/apimachinery/registered"
|
||||||
@ -28,6 +29,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/client/record"
|
"k8s.io/kubernetes/pkg/client/record"
|
||||||
"k8s.io/kubernetes/pkg/client/testing/core"
|
"k8s.io/kubernetes/pkg/client/testing/core"
|
||||||
"k8s.io/kubernetes/pkg/controller"
|
"k8s.io/kubernetes/pkg/controller"
|
||||||
|
"k8s.io/kubernetes/pkg/controller/deployment/util"
|
||||||
"k8s.io/kubernetes/pkg/controller/informers"
|
"k8s.io/kubernetes/pkg/controller/informers"
|
||||||
"k8s.io/kubernetes/pkg/runtime"
|
"k8s.io/kubernetes/pkg/runtime"
|
||||||
"k8s.io/kubernetes/pkg/runtime/schema"
|
"k8s.io/kubernetes/pkg/runtime/schema"
|
||||||
@ -67,9 +69,10 @@ func newDeployment(name string, replicas int, revisionHistoryLimit *int32, maxSu
|
|||||||
d := extensions.Deployment{
|
d := extensions.Deployment{
|
||||||
TypeMeta: metav1.TypeMeta{APIVersion: registered.GroupOrDie(extensions.GroupName).GroupVersion.String()},
|
TypeMeta: metav1.TypeMeta{APIVersion: registered.GroupOrDie(extensions.GroupName).GroupVersion.String()},
|
||||||
ObjectMeta: v1.ObjectMeta{
|
ObjectMeta: v1.ObjectMeta{
|
||||||
UID: uuid.NewUUID(),
|
UID: uuid.NewUUID(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Namespace: v1.NamespaceDefault,
|
Namespace: v1.NamespaceDefault,
|
||||||
|
Annotations: make(map[string]string),
|
||||||
},
|
},
|
||||||
Spec: extensions.DeploymentSpec{
|
Spec: extensions.DeploymentSpec{
|
||||||
Strategy: extensions.DeploymentStrategy{
|
Strategy: extensions.DeploymentStrategy{
|
||||||
@ -110,8 +113,10 @@ func newReplicaSet(d *extensions.Deployment, name string, replicas int) *extensi
|
|||||||
ObjectMeta: v1.ObjectMeta{
|
ObjectMeta: v1.ObjectMeta{
|
||||||
Name: name,
|
Name: name,
|
||||||
Namespace: v1.NamespaceDefault,
|
Namespace: v1.NamespaceDefault,
|
||||||
|
Labels: d.Spec.Selector.MatchLabels,
|
||||||
},
|
},
|
||||||
Spec: extensions.ReplicaSetSpec{
|
Spec: extensions.ReplicaSetSpec{
|
||||||
|
Selector: d.Spec.Selector,
|
||||||
Replicas: func() *int32 { i := int32(replicas); return &i }(),
|
Replicas: func() *int32 { i := int32(replicas); return &i }(),
|
||||||
Template: d.Spec.Template,
|
Template: d.Spec.Template,
|
||||||
},
|
},
|
||||||
@ -163,7 +168,7 @@ func newFixture(t *testing.T) *fixture {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fixture) run(deploymentName string) {
|
func (f *fixture) newController() (*DeploymentController, informers.SharedInformerFactory) {
|
||||||
f.client = fake.NewSimpleClientset(f.objects...)
|
f.client = fake.NewSimpleClientset(f.objects...)
|
||||||
informers := informers.NewSharedInformerFactory(f.client, nil, controller.NoResyncPeriodFunc())
|
informers := informers.NewSharedInformerFactory(f.client, nil, controller.NoResyncPeriodFunc())
|
||||||
c := NewDeploymentController(informers.Deployments(), informers.ReplicaSets(), informers.Pods(), f.client)
|
c := NewDeploymentController(informers.Deployments(), informers.ReplicaSets(), informers.Pods(), f.client)
|
||||||
@ -180,6 +185,11 @@ func (f *fixture) run(deploymentName string) {
|
|||||||
for _, pod := range f.podLister {
|
for _, pod := range f.podLister {
|
||||||
c.podLister.Indexer.Add(pod)
|
c.podLister.Indexer.Add(pod)
|
||||||
}
|
}
|
||||||
|
return c, informers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fixture) run(deploymentName string) {
|
||||||
|
c, informers := f.newController()
|
||||||
stopCh := make(chan struct{})
|
stopCh := make(chan struct{})
|
||||||
defer close(stopCh)
|
defer close(stopCh)
|
||||||
informers.Start(stopCh)
|
informers.Start(stopCh)
|
||||||
@ -286,3 +296,218 @@ func filterInformerActions(actions []core.Action) []core.Action {
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestOverlappingDeployment ensures that an overlapping deployment will not be synced by
|
||||||
|
// the controller.
|
||||||
|
func TestOverlappingDeployment(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
now := metav1.Now()
|
||||||
|
later := metav1.Time{Time: now.Add(time.Minute)}
|
||||||
|
|
||||||
|
foo := newDeployment("foo", 1, nil, nil, nil, map[string]string{"foo": "bar"})
|
||||||
|
foo.CreationTimestamp = now
|
||||||
|
bar := newDeployment("bar", 1, nil, nil, nil, map[string]string{"foo": "bar", "app": "baz"})
|
||||||
|
bar.CreationTimestamp = later
|
||||||
|
|
||||||
|
f.dLister = append(f.dLister, foo, bar)
|
||||||
|
f.objects = append(f.objects, foo, bar)
|
||||||
|
|
||||||
|
f.expectUpdateDeploymentStatusAction(bar)
|
||||||
|
f.run(getKey(bar, t))
|
||||||
|
|
||||||
|
actions := f.client.Actions()
|
||||||
|
d := actions[0].(core.UpdateAction).GetObject().(*extensions.Deployment)
|
||||||
|
if len(d.Annotations[util.OverlapAnnotation]) == 0 {
|
||||||
|
t.Errorf("annotations weren't updated for the overlapping deployment: %v", d.Annotations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSyncOverlappedDeployment ensures that from two overlapping deployments, the older
|
||||||
|
// one will be synced and the newer will be marked as overlapping. Note that in reality it's
|
||||||
|
// not always the older deployment that is the one that works vs the rest but the one which
|
||||||
|
// has the selector unchanged for longer time.
|
||||||
|
func TestSyncOverlappedDeployment(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
now := metav1.Now()
|
||||||
|
later := metav1.Time{Time: now.Add(time.Minute)}
|
||||||
|
|
||||||
|
foo := newDeployment("foo", 1, nil, nil, nil, map[string]string{"foo": "bar"})
|
||||||
|
foo.CreationTimestamp = now
|
||||||
|
bar := newDeployment("bar", 1, nil, nil, nil, map[string]string{"foo": "bar", "app": "baz"})
|
||||||
|
bar.CreationTimestamp = later
|
||||||
|
|
||||||
|
f.dLister = append(f.dLister, foo, bar)
|
||||||
|
f.objects = append(f.objects, foo, bar)
|
||||||
|
|
||||||
|
f.expectUpdateDeploymentStatusAction(bar)
|
||||||
|
f.expectCreateRSAction(newReplicaSet(foo, "foo-rs", 1))
|
||||||
|
f.expectUpdateDeploymentAction(foo)
|
||||||
|
f.expectUpdateDeploymentStatusAction(foo)
|
||||||
|
f.run(getKey(foo, t))
|
||||||
|
|
||||||
|
actions := f.client.Actions()
|
||||||
|
d := actions[0].(core.UpdateAction).GetObject().(*extensions.Deployment)
|
||||||
|
if d.Annotations[util.OverlapAnnotation] != "foo" {
|
||||||
|
t.Errorf("annotations weren't updated for the overlapping deployment: %v", d.Annotations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeletedDeploymentShouldCleanupOverlaps ensures that the deletion of a deployment
|
||||||
|
// will cleanup any deployments that overlap with it.
|
||||||
|
func TestDeletedDeploymentShouldCleanupOverlaps(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
now := metav1.Now()
|
||||||
|
earlier := metav1.Time{Time: now.Add(-time.Minute)}
|
||||||
|
later := metav1.Time{Time: now.Add(time.Minute)}
|
||||||
|
|
||||||
|
foo := newDeployment("foo", 1, nil, nil, nil, map[string]string{"foo": "bar"})
|
||||||
|
foo.CreationTimestamp = earlier
|
||||||
|
foo.DeletionTimestamp = &now
|
||||||
|
bar := newDeployment("bar", 1, nil, nil, nil, map[string]string{"foo": "bar"})
|
||||||
|
bar.CreationTimestamp = later
|
||||||
|
bar.Annotations = map[string]string{util.OverlapAnnotation: "foo"}
|
||||||
|
|
||||||
|
f.dLister = append(f.dLister, foo, bar)
|
||||||
|
f.objects = append(f.objects, foo, bar)
|
||||||
|
|
||||||
|
f.expectUpdateDeploymentStatusAction(bar)
|
||||||
|
f.expectUpdateDeploymentStatusAction(foo)
|
||||||
|
f.run(getKey(foo, t))
|
||||||
|
|
||||||
|
for _, a := range f.client.Actions() {
|
||||||
|
action, ok := a.(core.UpdateAction)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := action.GetObject().(*extensions.Deployment)
|
||||||
|
if d.Name != "bar" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Annotations[util.OverlapAnnotation]) > 0 {
|
||||||
|
t.Errorf("annotations weren't cleaned up for the overlapping deployment: %v", d.Annotations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeletedDeploymentShouldNotCleanupOtherOverlaps ensures that the deletion of
|
||||||
|
// a deployment will not cleanup deployments that overlap with another deployment.
|
||||||
|
func TestDeletedDeploymentShouldNotCleanupOtherOverlaps(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
now := metav1.Now()
|
||||||
|
earlier := metav1.Time{Time: now.Add(-time.Minute)}
|
||||||
|
later := metav1.Time{Time: now.Add(time.Minute)}
|
||||||
|
|
||||||
|
foo := newDeployment("foo", 1, nil, nil, nil, map[string]string{"foo": "bar"})
|
||||||
|
foo.CreationTimestamp = earlier
|
||||||
|
foo.DeletionTimestamp = &now
|
||||||
|
bar := newDeployment("bar", 1, nil, nil, nil, map[string]string{"bla": "bla"})
|
||||||
|
bar.CreationTimestamp = later
|
||||||
|
// Notice this deployment is overlapping with another deployment
|
||||||
|
bar.Annotations = map[string]string{util.OverlapAnnotation: "baz"}
|
||||||
|
|
||||||
|
f.dLister = append(f.dLister, foo, bar)
|
||||||
|
f.objects = append(f.objects, foo, bar)
|
||||||
|
|
||||||
|
f.expectUpdateDeploymentStatusAction(foo)
|
||||||
|
f.run(getKey(foo, t))
|
||||||
|
|
||||||
|
for _, a := range f.client.Actions() {
|
||||||
|
action, ok := a.(core.UpdateAction)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := action.GetObject().(*extensions.Deployment)
|
||||||
|
if d.Name != "bar" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Annotations[util.OverlapAnnotation]) == 0 {
|
||||||
|
t.Errorf("overlapping annotation should not be cleaned up for bar: %v", d.Annotations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPodDeletionEnqueuesRecreateDeployment ensures that the deletion of a pod
|
||||||
|
// will requeue a Recreate deployment iff there is no other pod returned from the
|
||||||
|
// client.
|
||||||
|
func TestPodDeletionEnqueuesRecreateDeployment(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
|
||||||
|
foo := newDeployment("foo", 1, nil, nil, nil, map[string]string{"foo": "bar"})
|
||||||
|
foo.Spec.Strategy.Type = extensions.RecreateDeploymentStrategyType
|
||||||
|
rs := newReplicaSet(foo, "foo-1", 1)
|
||||||
|
pod := generatePodFromRS(rs)
|
||||||
|
|
||||||
|
f.dLister = append(f.dLister, foo)
|
||||||
|
f.rsLister = append(f.rsLister, rs)
|
||||||
|
f.objects = append(f.objects, foo, rs)
|
||||||
|
|
||||||
|
c, informers := f.newController()
|
||||||
|
enqueued := false
|
||||||
|
c.enqueueDeployment = func(d *extensions.Deployment) {
|
||||||
|
if d.Name == "foo" {
|
||||||
|
enqueued = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
defer close(stopCh)
|
||||||
|
informers.Start(stopCh)
|
||||||
|
|
||||||
|
c.deletePod(pod)
|
||||||
|
|
||||||
|
if !enqueued {
|
||||||
|
t.Errorf("expected deployment %q to be queued after pod deletion", foo.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPodDeletionDoesntEnqueueRecreateDeployment ensures that the deletion of a pod
|
||||||
|
// will not requeue a Recreate deployment iff there are other pods returned from the
|
||||||
|
// client.
|
||||||
|
func TestPodDeletionDoesntEnqueueRecreateDeployment(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
|
||||||
|
foo := newDeployment("foo", 1, nil, nil, nil, map[string]string{"foo": "bar"})
|
||||||
|
foo.Spec.Strategy.Type = extensions.RecreateDeploymentStrategyType
|
||||||
|
rs := newReplicaSet(foo, "foo-1", 1)
|
||||||
|
pod := generatePodFromRS(rs)
|
||||||
|
|
||||||
|
f.dLister = append(f.dLister, foo)
|
||||||
|
f.rsLister = append(f.rsLister, rs)
|
||||||
|
// Let's pretend this is a different pod. The gist is that the pod lister needs to
|
||||||
|
// return a non-empty list.
|
||||||
|
f.podLister = append(f.podLister, pod)
|
||||||
|
|
||||||
|
c, informers := f.newController()
|
||||||
|
enqueued := false
|
||||||
|
c.enqueueDeployment = func(d *extensions.Deployment) {
|
||||||
|
if d.Name == "foo" {
|
||||||
|
enqueued = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
defer close(stopCh)
|
||||||
|
informers.Start(stopCh)
|
||||||
|
|
||||||
|
c.deletePod(pod)
|
||||||
|
|
||||||
|
if enqueued {
|
||||||
|
t.Errorf("expected deployment %q not to be queued after pod deletion", foo.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePodFromRS creates a pod, with the input ReplicaSet's selector and its template
|
||||||
|
func generatePodFromRS(rs *extensions.ReplicaSet) *v1.Pod {
|
||||||
|
trueVar := true
|
||||||
|
return &v1.Pod{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Name: rs.Name + "-pod",
|
||||||
|
Namespace: rs.Namespace,
|
||||||
|
Labels: rs.Spec.Selector.MatchLabels,
|
||||||
|
OwnerReferences: []metav1.OwnerReference{
|
||||||
|
{UID: rs.UID, APIVersion: "v1beta1", Kind: "ReplicaSet", Name: rs.Name, Controller: &trueVar},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: rs.Spec.Template.Spec,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user