mirror of https://github.com/k3s-io/k3s
1373 lines
39 KiB
Go
1373 lines
39 KiB
Go
/*
|
|
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 kubectl
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"k8s.io/kubernetes/pkg/api"
|
|
"k8s.io/kubernetes/pkg/api/testapi"
|
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
|
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
|
|
"k8s.io/kubernetes/pkg/runtime"
|
|
"k8s.io/kubernetes/pkg/util"
|
|
)
|
|
|
|
func oldRc(replicas int, original int) *api.ReplicationController {
|
|
return &api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Name: "foo-v1",
|
|
UID: "7764ae47-9092-11e4-8393-42010af018ff",
|
|
Annotations: map[string]string{
|
|
originalReplicasAnnotation: fmt.Sprintf("%d", original),
|
|
},
|
|
},
|
|
Spec: api.ReplicationControllerSpec{
|
|
Replicas: replicas,
|
|
Selector: map[string]string{"version": "v1"},
|
|
Template: &api.PodTemplateSpec{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Name: "foo-v1",
|
|
Labels: map[string]string{"version": "v1"},
|
|
},
|
|
},
|
|
},
|
|
Status: api.ReplicationControllerStatus{
|
|
Replicas: replicas,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newRc(replicas int, desired int) *api.ReplicationController {
|
|
rc := oldRc(replicas, replicas)
|
|
rc.Spec.Template = &api.PodTemplateSpec{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Name: "foo-v2",
|
|
Labels: map[string]string{"version": "v2"},
|
|
},
|
|
}
|
|
rc.Spec.Selector = map[string]string{"version": "v2"}
|
|
rc.ObjectMeta = api.ObjectMeta{
|
|
Name: "foo-v2",
|
|
Annotations: map[string]string{
|
|
desiredReplicasAnnotation: fmt.Sprintf("%d", desired),
|
|
sourceIdAnnotation: "foo-v1:7764ae47-9092-11e4-8393-42010af018ff",
|
|
},
|
|
}
|
|
return rc
|
|
}
|
|
|
|
// TestUpdate performs complex scenario testing for rolling updates. It
|
|
// provides fine grained control over the states for each update interval to
|
|
// allow the expression of as many edge cases as possible.
|
|
func TestUpdate(t *testing.T) {
|
|
// up represents a simulated scale up event and expectation
|
|
type up struct {
|
|
// to is the expected replica count for a scale-up
|
|
to int
|
|
}
|
|
// down represents a simulated scale down event and expectation
|
|
type down struct {
|
|
// oldReady is the number of oldRc replicas which will be seen
|
|
// as ready during the scale down attempt
|
|
oldReady int
|
|
// newReady is the number of newRc replicas which will be seen
|
|
// as ready during the scale up attempt
|
|
newReady int
|
|
// to is the expected replica count for the scale down
|
|
to int
|
|
// noop and to are mutually exclusive; if noop is true, that means for
|
|
// this down event, no scaling attempt should be made (for example, if
|
|
// by scaling down, the readiness minimum would be crossed.)
|
|
noop bool
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
// oldRc is the "from" deployment
|
|
oldRc *api.ReplicationController
|
|
// newRc is the "to" deployment
|
|
newRc *api.ReplicationController
|
|
// whether newRc existed (false means it was created)
|
|
newRcExists bool
|
|
maxUnavail util.IntOrString
|
|
maxSurge util.IntOrString
|
|
// expected is the sequence of up/down events that will be simulated and
|
|
// verified
|
|
expected []interface{}
|
|
// output is the expected textual output written
|
|
output string
|
|
}{
|
|
{
|
|
name: "10->10 30/0 fast readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("30%"),
|
|
maxSurge: util.NewIntOrStringFromString("0%"),
|
|
expected: []interface{}{
|
|
down{oldReady: 10, newReady: 0, to: 7},
|
|
up{3},
|
|
down{oldReady: 7, newReady: 3, to: 4},
|
|
up{6},
|
|
down{oldReady: 4, newReady: 6, to: 1},
|
|
up{9},
|
|
down{oldReady: 1, newReady: 9, to: 0},
|
|
up{10},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 7 pods available, don't exceed 10 pods)
|
|
Scaling foo-v1 down to 7
|
|
Scaling foo-v2 up to 3
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 6
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 9
|
|
Scaling foo-v1 down to 0
|
|
Scaling foo-v2 up to 10
|
|
`,
|
|
},
|
|
{
|
|
name: "10->10 30/0 delayed readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("30%"),
|
|
maxSurge: util.NewIntOrStringFromString("0%"),
|
|
expected: []interface{}{
|
|
down{oldReady: 10, newReady: 0, to: 7},
|
|
up{3},
|
|
down{oldReady: 7, newReady: 0, noop: true},
|
|
down{oldReady: 7, newReady: 1, to: 6},
|
|
up{4},
|
|
down{oldReady: 6, newReady: 4, to: 3},
|
|
up{7},
|
|
down{oldReady: 3, newReady: 7, to: 0},
|
|
up{10},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 7 pods available, don't exceed 10 pods)
|
|
Scaling foo-v1 down to 7
|
|
Scaling foo-v2 up to 3
|
|
Scaling foo-v1 down to 6
|
|
Scaling foo-v2 up to 4
|
|
Scaling foo-v1 down to 3
|
|
Scaling foo-v2 up to 7
|
|
Scaling foo-v1 down to 0
|
|
Scaling foo-v2 up to 10
|
|
`,
|
|
}, {
|
|
name: "10->10 30/0 fast readiness, continuation",
|
|
oldRc: oldRc(7, 10),
|
|
newRc: newRc(3, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("30%"),
|
|
maxSurge: util.NewIntOrStringFromString("0%"),
|
|
expected: []interface{}{
|
|
down{oldReady: 7, newReady: 3, to: 4},
|
|
up{6},
|
|
down{oldReady: 4, newReady: 6, to: 1},
|
|
up{9},
|
|
down{oldReady: 1, newReady: 9, to: 0},
|
|
up{10},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 3 to 10, scaling down foo-v1 from 7 to 0 (keep 7 pods available, don't exceed 10 pods)
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 6
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 9
|
|
Scaling foo-v1 down to 0
|
|
Scaling foo-v2 up to 10
|
|
`,
|
|
}, {
|
|
name: "10->10 30/0 fast readiness, continued after restart which prevented first scale-up",
|
|
oldRc: oldRc(7, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("30%"),
|
|
maxSurge: util.NewIntOrStringFromString("0%"),
|
|
expected: []interface{}{
|
|
down{oldReady: 7, newReady: 0, noop: true},
|
|
up{3},
|
|
down{oldReady: 7, newReady: 3, to: 4},
|
|
up{6},
|
|
down{oldReady: 4, newReady: 6, to: 1},
|
|
up{9},
|
|
down{oldReady: 1, newReady: 9, to: 0},
|
|
up{10},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 7 to 0 (keep 7 pods available, don't exceed 10 pods)
|
|
Scaling foo-v2 up to 3
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 6
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 9
|
|
Scaling foo-v1 down to 0
|
|
Scaling foo-v2 up to 10
|
|
`,
|
|
}, {
|
|
name: "10->10 0/30 fast readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("0%"),
|
|
maxSurge: util.NewIntOrStringFromString("30%"),
|
|
expected: []interface{}{
|
|
up{3},
|
|
down{oldReady: 10, newReady: 3, to: 7},
|
|
up{6},
|
|
down{oldReady: 7, newReady: 6, to: 4},
|
|
up{9},
|
|
down{oldReady: 4, newReady: 9, to: 1},
|
|
up{10},
|
|
down{oldReady: 1, newReady: 10, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 10 pods available, don't exceed 13 pods)
|
|
Scaling foo-v2 up to 3
|
|
Scaling foo-v1 down to 7
|
|
Scaling foo-v2 up to 6
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 9
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 0/30 delayed readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("0%"),
|
|
maxSurge: util.NewIntOrStringFromString("30%"),
|
|
expected: []interface{}{
|
|
up{3},
|
|
down{oldReady: 10, newReady: 0, noop: true},
|
|
down{oldReady: 10, newReady: 1, to: 9},
|
|
up{4},
|
|
down{oldReady: 9, newReady: 3, to: 7},
|
|
up{6},
|
|
down{oldReady: 7, newReady: 6, to: 4},
|
|
up{9},
|
|
down{oldReady: 4, newReady: 9, to: 1},
|
|
up{10},
|
|
down{oldReady: 1, newReady: 9, noop: true},
|
|
down{oldReady: 1, newReady: 10, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 10 pods available, don't exceed 13 pods)
|
|
Scaling foo-v2 up to 3
|
|
Scaling foo-v1 down to 9
|
|
Scaling foo-v2 up to 4
|
|
Scaling foo-v1 down to 7
|
|
Scaling foo-v2 up to 6
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 9
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 10/20 fast readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("10%"),
|
|
maxSurge: util.NewIntOrStringFromString("20%"),
|
|
expected: []interface{}{
|
|
up{2},
|
|
down{oldReady: 10, newReady: 2, to: 7},
|
|
up{5},
|
|
down{oldReady: 7, newReady: 5, to: 4},
|
|
up{8},
|
|
down{oldReady: 4, newReady: 8, to: 1},
|
|
up{10},
|
|
down{oldReady: 10, newReady: 1, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 9 pods available, don't exceed 12 pods)
|
|
Scaling foo-v2 up to 2
|
|
Scaling foo-v1 down to 7
|
|
Scaling foo-v2 up to 5
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 8
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 10/20 delayed readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("10%"),
|
|
maxSurge: util.NewIntOrStringFromString("20%"),
|
|
expected: []interface{}{
|
|
up{2},
|
|
down{oldReady: 10, newReady: 2, to: 7},
|
|
up{5},
|
|
down{oldReady: 7, newReady: 4, to: 5},
|
|
up{7},
|
|
down{oldReady: 5, newReady: 4, noop: true},
|
|
down{oldReady: 5, newReady: 7, to: 2},
|
|
up{10},
|
|
down{oldReady: 2, newReady: 9, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 9 pods available, don't exceed 12 pods)
|
|
Scaling foo-v2 up to 2
|
|
Scaling foo-v1 down to 7
|
|
Scaling foo-v2 up to 5
|
|
Scaling foo-v1 down to 5
|
|
Scaling foo-v2 up to 7
|
|
Scaling foo-v1 down to 2
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 10/20 fast readiness continued after restart which prevented first scale-down",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(2, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("10%"),
|
|
maxSurge: util.NewIntOrStringFromString("20%"),
|
|
expected: []interface{}{
|
|
down{oldReady: 10, newReady: 2, to: 7},
|
|
up{5},
|
|
down{oldReady: 7, newReady: 5, to: 4},
|
|
up{8},
|
|
down{oldReady: 4, newReady: 8, to: 1},
|
|
up{10},
|
|
down{oldReady: 1, newReady: 10, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 2 to 10, scaling down foo-v1 from 10 to 0 (keep 9 pods available, don't exceed 12 pods)
|
|
Scaling foo-v1 down to 7
|
|
Scaling foo-v2 up to 5
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 8
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 0/100 fast readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("0%"),
|
|
maxSurge: util.NewIntOrStringFromString("100%"),
|
|
expected: []interface{}{
|
|
up{10},
|
|
down{oldReady: 10, newReady: 10, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 10 pods available, don't exceed 20 pods)
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 0/100 delayed readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("0%"),
|
|
maxSurge: util.NewIntOrStringFromString("100%"),
|
|
expected: []interface{}{
|
|
up{10},
|
|
down{oldReady: 10, newReady: 0, noop: true},
|
|
down{oldReady: 10, newReady: 2, to: 8},
|
|
down{oldReady: 8, newReady: 7, to: 3},
|
|
down{oldReady: 3, newReady: 10, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 10 pods available, don't exceed 20 pods)
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 8
|
|
Scaling foo-v1 down to 3
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 100/0 fast readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("100%"),
|
|
maxSurge: util.NewIntOrStringFromString("0%"),
|
|
expected: []interface{}{
|
|
down{oldReady: 10, newReady: 0, to: 0},
|
|
up{10},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 10, scaling down foo-v1 from 10 to 0 (keep 0 pods available, don't exceed 10 pods)
|
|
Scaling foo-v1 down to 0
|
|
Scaling foo-v2 up to 10
|
|
`,
|
|
}, {
|
|
name: "1->1 10/0 fast readiness",
|
|
oldRc: oldRc(1, 1),
|
|
newRc: newRc(0, 1),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("10%"),
|
|
maxSurge: util.NewIntOrStringFromString("0%"),
|
|
expected: []interface{}{
|
|
down{oldReady: 1, newReady: 0, to: 0},
|
|
up{1},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 1, scaling down foo-v1 from 1 to 0 (keep 0 pods available, don't exceed 1 pods)
|
|
Scaling foo-v1 down to 0
|
|
Scaling foo-v2 up to 1
|
|
`,
|
|
}, {
|
|
name: "1->1 0/10 delayed readiness",
|
|
oldRc: oldRc(1, 1),
|
|
newRc: newRc(0, 1),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("0%"),
|
|
maxSurge: util.NewIntOrStringFromString("10%"),
|
|
expected: []interface{}{
|
|
up{1},
|
|
down{oldReady: 1, newReady: 0, noop: true},
|
|
down{oldReady: 1, newReady: 1, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 1, scaling down foo-v1 from 1 to 0 (keep 1 pods available, don't exceed 2 pods)
|
|
Scaling foo-v2 up to 1
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "1->1 10/10 delayed readiness",
|
|
oldRc: oldRc(1, 1),
|
|
newRc: newRc(0, 1),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("10%"),
|
|
maxSurge: util.NewIntOrStringFromString("10%"),
|
|
expected: []interface{}{
|
|
up{1},
|
|
down{oldReady: 1, newReady: 0, noop: true},
|
|
down{oldReady: 1, newReady: 1, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 1, scaling down foo-v1 from 1 to 0 (keep 0 pods available, don't exceed 2 pods)
|
|
Scaling foo-v2 up to 1
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "3->3 1/1 fast readiness (absolute values)",
|
|
oldRc: oldRc(3, 3),
|
|
newRc: newRc(0, 3),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromInt(0),
|
|
maxSurge: util.NewIntOrStringFromInt(1),
|
|
expected: []interface{}{
|
|
up{1},
|
|
down{oldReady: 3, newReady: 1, to: 2},
|
|
up{2},
|
|
down{oldReady: 2, newReady: 2, to: 1},
|
|
up{3},
|
|
down{oldReady: 1, newReady: 3, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 3, scaling down foo-v1 from 3 to 0 (keep 3 pods available, don't exceed 4 pods)
|
|
Scaling foo-v2 up to 1
|
|
Scaling foo-v1 down to 2
|
|
Scaling foo-v2 up to 2
|
|
Scaling foo-v1 down to 1
|
|
Scaling foo-v2 up to 3
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->10 0/20 fast readiness, continued after restart which resulted in partial first scale-up",
|
|
oldRc: oldRc(6, 10),
|
|
newRc: newRc(5, 10),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("0%"),
|
|
maxSurge: util.NewIntOrStringFromString("20%"),
|
|
expected: []interface{}{
|
|
up{6},
|
|
down{oldReady: 6, newReady: 6, to: 4},
|
|
up{8},
|
|
down{oldReady: 4, newReady: 8, to: 2},
|
|
up{10},
|
|
down{oldReady: 10, newReady: 2, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 5 to 10, scaling down foo-v1 from 6 to 0 (keep 10 pods available, don't exceed 12 pods)
|
|
Scaling foo-v2 up to 6
|
|
Scaling foo-v1 down to 4
|
|
Scaling foo-v2 up to 8
|
|
Scaling foo-v1 down to 2
|
|
Scaling foo-v2 up to 10
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
}, {
|
|
name: "10->20 0/300 fast readiness",
|
|
oldRc: oldRc(10, 10),
|
|
newRc: newRc(0, 20),
|
|
newRcExists: false,
|
|
maxUnavail: util.NewIntOrStringFromString("0%"),
|
|
maxSurge: util.NewIntOrStringFromString("300%"),
|
|
expected: []interface{}{
|
|
up{20},
|
|
down{oldReady: 10, newReady: 20, to: 0},
|
|
},
|
|
output: `Created foo-v2
|
|
Scaling up foo-v2 from 0 to 20, scaling down foo-v1 from 10 to 0 (keep 10 pods available, don't exceed 40 pods)
|
|
Scaling foo-v2 up to 20
|
|
Scaling foo-v1 down to 0
|
|
`,
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
// Extract expectations into some makeshift FIFOs so they can be returned
|
|
// in the correct order from the right places. This lets scale downs be
|
|
// expressed a single event even though the data is used from multiple
|
|
// interface calls.
|
|
oldReady := []int{}
|
|
newReady := []int{}
|
|
upTo := []int{}
|
|
downTo := []int{}
|
|
for _, event := range test.expected {
|
|
switch e := event.(type) {
|
|
case down:
|
|
oldReady = append(oldReady, e.oldReady)
|
|
newReady = append(newReady, e.newReady)
|
|
if !e.noop {
|
|
downTo = append(downTo, e.to)
|
|
}
|
|
case up:
|
|
upTo = append(upTo, e.to)
|
|
}
|
|
}
|
|
|
|
// Make a way to get the next item from our FIFOs. Returns -1 if the array
|
|
// is empty.
|
|
next := func(s *[]int) int {
|
|
slice := *s
|
|
v := -1
|
|
if len(slice) > 0 {
|
|
v = slice[0]
|
|
if len(slice) > 1 {
|
|
*s = slice[1:]
|
|
} else {
|
|
*s = []int{}
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
t.Logf("running test %d (%s) (up: %v, down: %v, oldReady: %v, newReady: %v)", i, test.name, upTo, downTo, oldReady, newReady)
|
|
updater := &RollingUpdater{
|
|
ns: "default",
|
|
scaleAndWait: func(rc *api.ReplicationController, retry *RetryParams, wait *RetryParams) (*api.ReplicationController, error) {
|
|
// Return a scale up or scale down expectation depending on the rc,
|
|
// and throw errors if there is no expectation expressed for this
|
|
// call.
|
|
expected := -1
|
|
switch {
|
|
case rc == test.newRc:
|
|
t.Logf("scaling up %s:%d", rc.Name, rc.Spec.Replicas)
|
|
expected = next(&upTo)
|
|
case rc == test.oldRc:
|
|
t.Logf("scaling down %s:%d", rc.Name, rc.Spec.Replicas)
|
|
expected = next(&downTo)
|
|
}
|
|
if expected == -1 {
|
|
t.Fatalf("unexpected scale of %s to %d", rc.Name, rc.Spec.Replicas)
|
|
} else if e, a := expected, rc.Spec.Replicas; e != a {
|
|
t.Fatalf("expected scale of %s to %d, got %d", rc.Name, e, a)
|
|
}
|
|
// Simulate the scale.
|
|
rc.Status.Replicas = rc.Spec.Replicas
|
|
return rc, nil
|
|
},
|
|
getOrCreateTargetController: func(controller *api.ReplicationController, sourceId string) (*api.ReplicationController, bool, error) {
|
|
// Simulate a create vs. update of an existing controller.
|
|
return test.newRc, test.newRcExists, nil
|
|
},
|
|
cleanup: func(oldRc, newRc *api.ReplicationController, config *RollingUpdaterConfig) error {
|
|
return nil
|
|
},
|
|
}
|
|
// Set up a mock readiness check which handles the test assertions.
|
|
updater.waitForReadyPods = func(interval, timeout time.Duration, oldRc, newRc *api.ReplicationController) (int, int, error) {
|
|
// Return simulated readiness, and throw an error if this call has no
|
|
// expectations defined.
|
|
oldReady := next(&oldReady)
|
|
newReady := next(&newReady)
|
|
if oldReady == -1 || newReady == -1 {
|
|
t.Fatalf("unexpected waitForReadyPods call for:\noldRc: %+v\nnewRc: %+v", oldRc, newRc)
|
|
}
|
|
return oldReady, newReady, nil
|
|
}
|
|
var buffer bytes.Buffer
|
|
config := &RollingUpdaterConfig{
|
|
Out: &buffer,
|
|
OldRc: test.oldRc,
|
|
NewRc: test.newRc,
|
|
UpdatePeriod: 0,
|
|
Interval: time.Millisecond,
|
|
Timeout: time.Millisecond,
|
|
CleanupPolicy: DeleteRollingUpdateCleanupPolicy,
|
|
MaxUnavailable: test.maxUnavail,
|
|
MaxSurge: test.maxSurge,
|
|
}
|
|
err := updater.Update(config)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != test.output {
|
|
t.Errorf("Bad output. expected:\n%s\ngot:\n%s", test.output, buffer.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUpdate_progressTimeout ensures that an update which isn't making any
|
|
// progress will eventually time out with a specified error.
|
|
func TestUpdate_progressTimeout(t *testing.T) {
|
|
oldRc := oldRc(2, 2)
|
|
newRc := newRc(0, 2)
|
|
updater := &RollingUpdater{
|
|
ns: "default",
|
|
scaleAndWait: func(rc *api.ReplicationController, retry *RetryParams, wait *RetryParams) (*api.ReplicationController, error) {
|
|
// Do nothing.
|
|
return rc, nil
|
|
},
|
|
getOrCreateTargetController: func(controller *api.ReplicationController, sourceId string) (*api.ReplicationController, bool, error) {
|
|
return newRc, false, nil
|
|
},
|
|
cleanup: func(oldRc, newRc *api.ReplicationController, config *RollingUpdaterConfig) error {
|
|
return nil
|
|
},
|
|
}
|
|
updater.waitForReadyPods = func(interval, timeout time.Duration, oldRc, newRc *api.ReplicationController) (int, int, error) {
|
|
// Coerce a timeout by pods never becoming ready.
|
|
return 0, 0, nil
|
|
}
|
|
var buffer bytes.Buffer
|
|
config := &RollingUpdaterConfig{
|
|
Out: &buffer,
|
|
OldRc: oldRc,
|
|
NewRc: newRc,
|
|
UpdatePeriod: 0,
|
|
Interval: time.Millisecond,
|
|
Timeout: time.Millisecond,
|
|
CleanupPolicy: DeleteRollingUpdateCleanupPolicy,
|
|
MaxUnavailable: util.NewIntOrStringFromInt(0),
|
|
MaxSurge: util.NewIntOrStringFromInt(1),
|
|
}
|
|
err := updater.Update(config)
|
|
if err == nil {
|
|
t.Fatalf("expected an error")
|
|
}
|
|
if e, a := "timed out waiting for any update progress to be made", err.Error(); e != a {
|
|
t.Fatalf("expected error message: %s, got: %s", e, a)
|
|
}
|
|
}
|
|
|
|
func TestUpdate_assignOriginalAnnotation(t *testing.T) {
|
|
oldRc := oldRc(1, 1)
|
|
delete(oldRc.Annotations, originalReplicasAnnotation)
|
|
newRc := newRc(1, 1)
|
|
var updatedOldRc *api.ReplicationController
|
|
fake := &testclient.Fake{}
|
|
fake.AddReactor("*", "*", func(action testclient.Action) (handled bool, ret runtime.Object, err error) {
|
|
switch a := action.(type) {
|
|
case testclient.GetAction:
|
|
return true, oldRc, nil
|
|
case testclient.UpdateAction:
|
|
updatedOldRc = a.GetObject().(*api.ReplicationController)
|
|
return true, updatedOldRc, nil
|
|
}
|
|
return false, nil, nil
|
|
})
|
|
updater := &RollingUpdater{
|
|
c: fake,
|
|
ns: "default",
|
|
scaleAndWait: func(rc *api.ReplicationController, retry *RetryParams, wait *RetryParams) (*api.ReplicationController, error) {
|
|
return rc, nil
|
|
},
|
|
getOrCreateTargetController: func(controller *api.ReplicationController, sourceId string) (*api.ReplicationController, bool, error) {
|
|
return newRc, false, nil
|
|
},
|
|
cleanup: func(oldRc, newRc *api.ReplicationController, config *RollingUpdaterConfig) error {
|
|
return nil
|
|
},
|
|
waitForReadyPods: func(interval, timeout time.Duration, oldRc, newRc *api.ReplicationController) (int, int, error) {
|
|
return 1, 1, nil
|
|
},
|
|
}
|
|
var buffer bytes.Buffer
|
|
config := &RollingUpdaterConfig{
|
|
Out: &buffer,
|
|
OldRc: oldRc,
|
|
NewRc: newRc,
|
|
UpdatePeriod: 0,
|
|
Interval: time.Millisecond,
|
|
Timeout: time.Millisecond,
|
|
CleanupPolicy: DeleteRollingUpdateCleanupPolicy,
|
|
MaxUnavailable: util.NewIntOrStringFromString("100%"),
|
|
}
|
|
err := updater.Update(config)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if updatedOldRc == nil {
|
|
t.Fatalf("expected rc to be updated")
|
|
}
|
|
if e, a := "1", updatedOldRc.Annotations[originalReplicasAnnotation]; e != a {
|
|
t.Fatalf("expected annotation value %s, got %s", e, a)
|
|
}
|
|
}
|
|
|
|
// TestRollingUpdater_cleanupWithClients ensures that the cleanup policy is
|
|
// correctly implemented.
|
|
func TestRollingUpdater_cleanupWithClients(t *testing.T) {
|
|
rc := oldRc(2, 2)
|
|
rcExisting := newRc(1, 3)
|
|
|
|
tests := []struct {
|
|
name string
|
|
policy RollingUpdaterCleanupPolicy
|
|
responses []runtime.Object
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "preserve",
|
|
policy: PreserveRollingUpdateCleanupPolicy,
|
|
responses: []runtime.Object{rcExisting},
|
|
expected: []string{
|
|
"get",
|
|
"update",
|
|
"get",
|
|
"get",
|
|
},
|
|
},
|
|
{
|
|
name: "delete",
|
|
policy: DeleteRollingUpdateCleanupPolicy,
|
|
responses: []runtime.Object{rcExisting},
|
|
expected: []string{
|
|
"get",
|
|
"update",
|
|
"get",
|
|
"get",
|
|
"delete",
|
|
},
|
|
},
|
|
{
|
|
name: "rename",
|
|
policy: RenameRollingUpdateCleanupPolicy,
|
|
responses: []runtime.Object{rcExisting},
|
|
expected: []string{
|
|
"get",
|
|
"update",
|
|
"get",
|
|
"get",
|
|
"delete",
|
|
"create",
|
|
"delete",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
fake := testclient.NewSimpleFake(test.responses...)
|
|
updater := &RollingUpdater{
|
|
ns: "default",
|
|
c: fake,
|
|
}
|
|
config := &RollingUpdaterConfig{
|
|
Out: ioutil.Discard,
|
|
OldRc: rc,
|
|
NewRc: rcExisting,
|
|
UpdatePeriod: 0,
|
|
Interval: time.Millisecond,
|
|
Timeout: time.Millisecond,
|
|
CleanupPolicy: test.policy,
|
|
}
|
|
err := updater.cleanupWithClients(rc, rcExisting, config)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if len(fake.Actions()) != len(test.expected) {
|
|
t.Fatalf("%s: unexpected actions: %v, expected %v", test.name, fake.Actions, test.expected)
|
|
}
|
|
for j, action := range fake.Actions() {
|
|
if e, a := test.expected[j], action.GetVerb(); e != a {
|
|
t.Errorf("%s: unexpected action: expected %s, got %s", test.name, e, a)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindSourceController(t *testing.T) {
|
|
ctrl1 := api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Name: "foo",
|
|
Annotations: map[string]string{
|
|
sourceIdAnnotation: "bar:1234",
|
|
},
|
|
},
|
|
}
|
|
ctrl2 := api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Name: "bar",
|
|
Annotations: map[string]string{
|
|
sourceIdAnnotation: "foo:12345",
|
|
},
|
|
},
|
|
}
|
|
ctrl3 := api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Annotations: map[string]string{
|
|
sourceIdAnnotation: "baz:45667",
|
|
},
|
|
},
|
|
}
|
|
tests := []struct {
|
|
list *api.ReplicationControllerList
|
|
expectedController *api.ReplicationController
|
|
err error
|
|
name string
|
|
expectError bool
|
|
}{
|
|
{
|
|
list: &api.ReplicationControllerList{},
|
|
expectError: true,
|
|
},
|
|
{
|
|
list: &api.ReplicationControllerList{
|
|
Items: []api.ReplicationController{ctrl1},
|
|
},
|
|
name: "foo",
|
|
expectError: true,
|
|
},
|
|
{
|
|
list: &api.ReplicationControllerList{
|
|
Items: []api.ReplicationController{ctrl1},
|
|
},
|
|
name: "bar",
|
|
expectedController: &ctrl1,
|
|
},
|
|
{
|
|
list: &api.ReplicationControllerList{
|
|
Items: []api.ReplicationController{ctrl1, ctrl2},
|
|
},
|
|
name: "bar",
|
|
expectedController: &ctrl1,
|
|
},
|
|
{
|
|
list: &api.ReplicationControllerList{
|
|
Items: []api.ReplicationController{ctrl1, ctrl2},
|
|
},
|
|
name: "foo",
|
|
expectedController: &ctrl2,
|
|
},
|
|
{
|
|
list: &api.ReplicationControllerList{
|
|
Items: []api.ReplicationController{ctrl1, ctrl2, ctrl3},
|
|
},
|
|
name: "baz",
|
|
expectedController: &ctrl3,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
fakeClient := testclient.NewSimpleFake(test.list)
|
|
ctrl, err := FindSourceController(fakeClient, "default", test.name)
|
|
if test.expectError && err == nil {
|
|
t.Errorf("unexpected non-error")
|
|
}
|
|
if !test.expectError && err != nil {
|
|
t.Errorf("unexpected error")
|
|
}
|
|
if !reflect.DeepEqual(ctrl, test.expectedController) {
|
|
t.Errorf("expected:\n%v\ngot:\n%v\n", test.expectedController, ctrl)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateExistingReplicationController(t *testing.T) {
|
|
tests := []struct {
|
|
rc *api.ReplicationController
|
|
name string
|
|
deploymentKey string
|
|
deploymentValue string
|
|
|
|
expectedRc *api.ReplicationController
|
|
expectErr bool
|
|
}{
|
|
{
|
|
rc: &api.ReplicationController{
|
|
Spec: api.ReplicationControllerSpec{
|
|
Template: &api.PodTemplateSpec{},
|
|
},
|
|
},
|
|
name: "foo",
|
|
deploymentKey: "dk",
|
|
deploymentValue: "some-hash",
|
|
|
|
expectedRc: &api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Annotations: map[string]string{
|
|
"kubectl.kubernetes.io/next-controller-id": "foo",
|
|
},
|
|
},
|
|
Spec: api.ReplicationControllerSpec{
|
|
Selector: map[string]string{
|
|
"dk": "some-hash",
|
|
},
|
|
Template: &api.PodTemplateSpec{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"dk": "some-hash",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
rc: &api.ReplicationController{
|
|
Spec: api.ReplicationControllerSpec{
|
|
Template: &api.PodTemplateSpec{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"dk": "some-other-hash",
|
|
},
|
|
},
|
|
},
|
|
Selector: map[string]string{
|
|
"dk": "some-other-hash",
|
|
},
|
|
},
|
|
},
|
|
name: "foo",
|
|
deploymentKey: "dk",
|
|
deploymentValue: "some-hash",
|
|
|
|
expectedRc: &api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Annotations: map[string]string{
|
|
"kubectl.kubernetes.io/next-controller-id": "foo",
|
|
},
|
|
},
|
|
Spec: api.ReplicationControllerSpec{
|
|
Selector: map[string]string{
|
|
"dk": "some-other-hash",
|
|
},
|
|
Template: &api.PodTemplateSpec{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"dk": "some-other-hash",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
buffer := &bytes.Buffer{}
|
|
fakeClient := testclient.NewSimpleFake(test.expectedRc)
|
|
rc, err := UpdateExistingReplicationController(fakeClient, test.rc, "default", test.name, test.deploymentKey, test.deploymentValue, buffer)
|
|
if !reflect.DeepEqual(rc, test.expectedRc) {
|
|
t.Errorf("expected:\n%#v\ngot:\n%#v\n", test.expectedRc, rc)
|
|
}
|
|
if test.expectErr && err == nil {
|
|
t.Errorf("unexpected non-error")
|
|
}
|
|
if !test.expectErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateWithRetries(t *testing.T) {
|
|
codec := testapi.Default.Codec()
|
|
grace := int64(30)
|
|
rc := &api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{Name: "rc",
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
Spec: api.ReplicationControllerSpec{
|
|
Selector: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Template: &api.PodTemplateSpec{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
Spec: api.PodSpec{
|
|
RestartPolicy: api.RestartPolicyAlways,
|
|
DNSPolicy: api.DNSClusterFirst,
|
|
TerminationGracePeriodSeconds: &grace,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test end to end updating of the rc with retries. Essentially make sure the update handler
|
|
// sees the right updates, failures in update/get are handled properly, and that the updated
|
|
// rc with new resource version is returned to the caller. Without any of these rollingupdate
|
|
// will fail cryptically.
|
|
newRc := *rc
|
|
newRc.ResourceVersion = "2"
|
|
newRc.Spec.Selector["baz"] = "foobar"
|
|
updates := []*http.Response{
|
|
{StatusCode: 500, Body: objBody(codec, &api.ReplicationController{})},
|
|
{StatusCode: 500, Body: objBody(codec, &api.ReplicationController{})},
|
|
{StatusCode: 200, Body: objBody(codec, &newRc)},
|
|
}
|
|
gets := []*http.Response{
|
|
{StatusCode: 500, Body: objBody(codec, &api.ReplicationController{})},
|
|
{StatusCode: 200, Body: objBody(codec, rc)},
|
|
}
|
|
fakeClient := &client.FakeRESTClient{
|
|
Codec: codec,
|
|
Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
|
|
switch p, m := req.URL.Path, req.Method; {
|
|
case p == testapi.Default.ResourcePath("replicationcontrollers", "default", "rc") && m == "PUT":
|
|
update := updates[0]
|
|
updates = updates[1:]
|
|
// We should always get an update with a valid rc even when the get fails. The rc should always
|
|
// contain the update.
|
|
if c, ok := readOrDie(t, req, codec).(*api.ReplicationController); !ok || !reflect.DeepEqual(rc, c) {
|
|
t.Errorf("Unexpected update body, got %+v expected %+v", c, rc)
|
|
} else if sel, ok := c.Spec.Selector["baz"]; !ok || sel != "foobar" {
|
|
t.Errorf("Expected selector label update, got %+v", c.Spec.Selector)
|
|
} else {
|
|
delete(c.Spec.Selector, "baz")
|
|
}
|
|
return update, nil
|
|
case p == testapi.Default.ResourcePath("replicationcontrollers", "default", "rc") && m == "GET":
|
|
get := gets[0]
|
|
gets = gets[1:]
|
|
return get, nil
|
|
default:
|
|
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
|
return nil, nil
|
|
}
|
|
}),
|
|
}
|
|
clientConfig := &client.Config{Version: testapi.Default.Version()}
|
|
client := client.NewOrDie(clientConfig)
|
|
client.Client = fakeClient.Client
|
|
|
|
if rc, err := updateWithRetries(
|
|
client.ReplicationControllers("default"), rc, func(c *api.ReplicationController) {
|
|
c.Spec.Selector["baz"] = "foobar"
|
|
}); err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
} else if sel, ok := rc.Spec.Selector["baz"]; !ok || sel != "foobar" || rc.ResourceVersion != "2" {
|
|
t.Errorf("Expected updated rc, got %+v", rc)
|
|
}
|
|
if len(updates) != 0 || len(gets) != 0 {
|
|
t.Errorf("Remaining updates %+v gets %+v", updates, gets)
|
|
}
|
|
}
|
|
|
|
func readOrDie(t *testing.T, req *http.Request, codec runtime.Codec) runtime.Object {
|
|
data, err := ioutil.ReadAll(req.Body)
|
|
if err != nil {
|
|
t.Errorf("Error reading: %v", err)
|
|
t.FailNow()
|
|
}
|
|
obj, err := codec.Decode(data)
|
|
if err != nil {
|
|
t.Errorf("error decoding: %v", err)
|
|
t.FailNow()
|
|
}
|
|
return obj
|
|
}
|
|
|
|
func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
|
|
return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
|
|
}
|
|
|
|
func TestAddDeploymentHash(t *testing.T) {
|
|
buf := &bytes.Buffer{}
|
|
codec := testapi.Default.Codec()
|
|
rc := &api.ReplicationController{
|
|
ObjectMeta: api.ObjectMeta{Name: "rc"},
|
|
Spec: api.ReplicationControllerSpec{
|
|
Selector: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Template: &api.PodTemplateSpec{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
podList := &api.PodList{
|
|
Items: []api.Pod{
|
|
{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
|
{ObjectMeta: api.ObjectMeta{Name: "bar"}},
|
|
{ObjectMeta: api.ObjectMeta{Name: "baz"}},
|
|
},
|
|
}
|
|
|
|
seen := util.StringSet{}
|
|
updatedRc := false
|
|
fakeClient := &client.FakeRESTClient{
|
|
Codec: codec,
|
|
Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
|
|
switch p, m := req.URL.Path, req.Method; {
|
|
case p == testapi.Default.ResourcePath("pods", "default", "") && m == "GET":
|
|
if req.URL.RawQuery != "labelSelector=foo%3Dbar" {
|
|
t.Errorf("Unexpected query string: %s", req.URL.RawQuery)
|
|
}
|
|
return &http.Response{StatusCode: 200, Body: objBody(codec, podList)}, nil
|
|
case p == testapi.Default.ResourcePath("pods", "default", "foo") && m == "PUT":
|
|
seen.Insert("foo")
|
|
obj := readOrDie(t, req, codec)
|
|
podList.Items[0] = *(obj.(*api.Pod))
|
|
return &http.Response{StatusCode: 200, Body: objBody(codec, &podList.Items[0])}, nil
|
|
case p == testapi.Default.ResourcePath("pods", "default", "bar") && m == "PUT":
|
|
seen.Insert("bar")
|
|
obj := readOrDie(t, req, codec)
|
|
podList.Items[1] = *(obj.(*api.Pod))
|
|
return &http.Response{StatusCode: 200, Body: objBody(codec, &podList.Items[1])}, nil
|
|
case p == testapi.Default.ResourcePath("pods", "default", "baz") && m == "PUT":
|
|
seen.Insert("baz")
|
|
obj := readOrDie(t, req, codec)
|
|
podList.Items[2] = *(obj.(*api.Pod))
|
|
return &http.Response{StatusCode: 200, Body: objBody(codec, &podList.Items[2])}, nil
|
|
case p == testapi.Default.ResourcePath("replicationcontrollers", "default", "rc") && m == "PUT":
|
|
updatedRc = true
|
|
return &http.Response{StatusCode: 200, Body: objBody(codec, rc)}, nil
|
|
default:
|
|
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
|
return nil, nil
|
|
}
|
|
}),
|
|
}
|
|
clientConfig := &client.Config{Version: testapi.Default.Version()}
|
|
client := client.NewOrDie(clientConfig)
|
|
client.Client = fakeClient.Client
|
|
|
|
if _, err := AddDeploymentKeyToReplicationController(rc, client, "dk", "hash", api.NamespaceDefault, buf); err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
for _, pod := range podList.Items {
|
|
if !seen.Has(pod.Name) {
|
|
t.Errorf("Missing update for pod: %s", pod.Name)
|
|
}
|
|
}
|
|
if !updatedRc {
|
|
t.Errorf("Failed to update replication controller with new labels")
|
|
}
|
|
}
|
|
|
|
func TestRollingUpdater_pollForReadyPods(t *testing.T) {
|
|
mkpod := func(owner *api.ReplicationController, ready bool) *api.Pod {
|
|
labels := map[string]string{}
|
|
for k, v := range owner.Spec.Selector {
|
|
labels[k] = v
|
|
}
|
|
status := api.ConditionTrue
|
|
if !ready {
|
|
status = api.ConditionFalse
|
|
}
|
|
return &api.Pod{
|
|
ObjectMeta: api.ObjectMeta{
|
|
Name: "pod",
|
|
Labels: labels,
|
|
},
|
|
Status: api.PodStatus{
|
|
Conditions: []api.PodCondition{
|
|
{
|
|
Type: api.PodReady,
|
|
Status: status,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
oldRc *api.ReplicationController
|
|
newRc *api.ReplicationController
|
|
// expectated old/new ready counts
|
|
oldReady int
|
|
newReady int
|
|
// pods owned by the rcs; indicate whether they're ready
|
|
oldPods []bool
|
|
newPods []bool
|
|
}{
|
|
{
|
|
oldRc: oldRc(4, 4),
|
|
newRc: newRc(4, 4),
|
|
oldReady: 4,
|
|
newReady: 2,
|
|
oldPods: []bool{
|
|
true,
|
|
true,
|
|
true,
|
|
true,
|
|
},
|
|
newPods: []bool{
|
|
true,
|
|
false,
|
|
true,
|
|
false,
|
|
},
|
|
},
|
|
{
|
|
oldRc: oldRc(4, 4),
|
|
newRc: newRc(4, 4),
|
|
oldReady: 0,
|
|
newReady: 1,
|
|
oldPods: []bool{
|
|
false,
|
|
},
|
|
newPods: []bool{
|
|
true,
|
|
},
|
|
},
|
|
{
|
|
oldRc: oldRc(4, 4),
|
|
newRc: newRc(4, 4),
|
|
oldReady: 1,
|
|
newReady: 0,
|
|
oldPods: []bool{
|
|
true,
|
|
},
|
|
newPods: []bool{
|
|
false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Logf("evaluating test %d", i)
|
|
// Populate the fake client with pods associated with their owners.
|
|
pods := []runtime.Object{}
|
|
for _, ready := range test.oldPods {
|
|
pods = append(pods, mkpod(test.oldRc, ready))
|
|
}
|
|
for _, ready := range test.newPods {
|
|
pods = append(pods, mkpod(test.newRc, ready))
|
|
}
|
|
client := testclient.NewSimpleFake(pods...)
|
|
|
|
updater := &RollingUpdater{
|
|
ns: "default",
|
|
c: client,
|
|
}
|
|
oldReady, newReady, err := updater.pollForReadyPods(time.Millisecond, time.Second, test.oldRc, test.newRc)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if e, a := test.oldReady, oldReady; e != a {
|
|
t.Errorf("expected old ready %d, got %d", e, a)
|
|
}
|
|
if e, a := test.newReady, newReady; e != a {
|
|
t.Errorf("expected new ready %d, got %d", e, a)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRollingUpdater_extractMaxValue(t *testing.T) {
|
|
tests := []struct {
|
|
field util.IntOrString
|
|
original int
|
|
expected int
|
|
valid bool
|
|
}{
|
|
{
|
|
field: util.NewIntOrStringFromInt(1),
|
|
original: 100,
|
|
expected: 1,
|
|
valid: true,
|
|
},
|
|
{
|
|
field: util.NewIntOrStringFromInt(0),
|
|
original: 100,
|
|
expected: 0,
|
|
valid: true,
|
|
},
|
|
{
|
|
field: util.NewIntOrStringFromInt(-1),
|
|
original: 100,
|
|
valid: false,
|
|
},
|
|
{
|
|
field: util.NewIntOrStringFromString("10%"),
|
|
original: 100,
|
|
expected: 10,
|
|
valid: true,
|
|
},
|
|
{
|
|
field: util.NewIntOrStringFromString("100%"),
|
|
original: 100,
|
|
expected: 100,
|
|
valid: true,
|
|
},
|
|
{
|
|
field: util.NewIntOrStringFromString("200%"),
|
|
original: 100,
|
|
expected: 200,
|
|
valid: true,
|
|
},
|
|
{
|
|
field: util.NewIntOrStringFromString("0%"),
|
|
original: 100,
|
|
expected: 0,
|
|
valid: true,
|
|
},
|
|
{
|
|
field: util.NewIntOrStringFromString("-1%"),
|
|
original: 100,
|
|
valid: false,
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Logf("evaluating test %d", i)
|
|
max, err := extractMaxValue(test.field, "field", test.original)
|
|
if test.valid && err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !test.valid && err == nil {
|
|
t.Fatalf("expected an error")
|
|
}
|
|
if e, a := test.expected, max; e != a {
|
|
t.Fatalf("expected max %d, got %d", e, a)
|
|
}
|
|
}
|
|
}
|