Make pod listing costant-time

* move ip cache out of registry/pod
* combine, rationalize, and move pod status logic
* Fix unit and integration tests
This commit is contained in:
Daniel Smith
2014-12-20 18:49:10 -08:00
parent 9b6aec5e22
commit 5b8e91595a
8 changed files with 1057 additions and 872 deletions

View File

@@ -19,9 +19,12 @@ package master
import (
"reflect"
"testing"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
type FakePodInfoGetter struct {
@@ -40,128 +43,660 @@ func (f *FakePodInfoGetter) GetPodInfo(host, namespace, id string) (api.PodConta
}
func TestPodCacheGetDifferentNamespace(t *testing.T) {
cache := NewPodCache(nil, nil)
cache := NewPodCache(nil, nil, nil, nil)
expectedDefault := api.PodContainerInfo{
ContainerInfo: api.PodInfo{
expectedDefault := api.PodStatus{
Info: api.PodInfo{
"foo": api.ContainerStatus{},
},
}
expectedOther := api.PodContainerInfo{
ContainerInfo: api.PodInfo{
expectedOther := api.PodStatus{
Info: api.PodInfo{
"bar": api.ContainerStatus{},
},
}
cache.podInfo[makePodCacheKey(api.NamespaceDefault, "foo")] = expectedDefault
cache.podInfo[makePodCacheKey("other", "foo")] = expectedOther
cache.podStatus[objKey{api.NamespaceDefault, "foo"}] = expectedDefault
cache.podStatus[objKey{"other", "foo"}] = expectedOther
info, err := cache.GetPodInfo("host", api.NamespaceDefault, "foo")
info, err := cache.GetPodStatus(api.NamespaceDefault, "foo")
if err != nil {
t.Errorf("Unexpected error: %#v", err)
t.Errorf("Unexpected error: %+v", err)
}
if !reflect.DeepEqual(info, expectedDefault) {
t.Errorf("Unexpected mismatch. Expected: %#v, Got: #%v", &expectedOther, info)
if !reflect.DeepEqual(info, &expectedDefault) {
t.Errorf("Unexpected mismatch. Expected: %+v, Got: %+v", &expectedOther, info)
}
info, err = cache.GetPodInfo("host", "other", "foo")
info, err = cache.GetPodStatus("other", "foo")
if err != nil {
t.Errorf("Unexpected error: %#v", err)
t.Errorf("Unexpected error: %+v", err)
}
if !reflect.DeepEqual(info, expectedOther) {
t.Errorf("Unexpected mismatch. Expected: %#v, Got: #%v", &expectedOther, info)
if !reflect.DeepEqual(info, &expectedOther) {
t.Errorf("Unexpected mismatch. Expected: %+v, Got: %+v", &expectedOther, info)
}
}
func TestPodCacheGet(t *testing.T) {
cache := NewPodCache(nil, nil)
cache := NewPodCache(nil, nil, nil, nil)
expected := api.PodContainerInfo{
ContainerInfo: api.PodInfo{
expected := api.PodStatus{
Info: api.PodInfo{
"foo": api.ContainerStatus{},
},
}
cache.podInfo[makePodCacheKey(api.NamespaceDefault, "foo")] = expected
cache.podStatus[objKey{api.NamespaceDefault, "foo"}] = expected
info, err := cache.GetPodInfo("host", api.NamespaceDefault, "foo")
info, err := cache.GetPodStatus(api.NamespaceDefault, "foo")
if err != nil {
t.Errorf("Unexpected error: %#v", err)
t.Errorf("Unexpected error: %+v", err)
}
if !reflect.DeepEqual(info, expected) {
t.Errorf("Unexpected mismatch. Expected: %#v, Got: #%v", &expected, info)
if !reflect.DeepEqual(info, &expected) {
t.Errorf("Unexpected mismatch. Expected: %+v, Got: %+v", &expected, info)
}
}
func TestPodCacheGetMissing(t *testing.T) {
cache := NewPodCache(nil, nil)
cache := NewPodCache(nil, nil, nil, nil)
info, err := cache.GetPodInfo("host", api.NamespaceDefault, "foo")
status, err := cache.GetPodStatus(api.NamespaceDefault, "foo")
if err == nil {
t.Errorf("Unexpected non-error: %#v", err)
t.Errorf("Unexpected non-error: %+v", err)
}
if !reflect.DeepEqual(info, api.PodContainerInfo{}) {
t.Errorf("Unexpected info: %#v", info)
if status != nil {
t.Errorf("Unexpected status: %+v", status)
}
}
func TestPodGetPodInfoGetter(t *testing.T) {
expected := api.PodContainerInfo{
ContainerInfo: api.PodInfo{
"foo": api.ContainerStatus{},
type fakeIPCache func(string) string
func (f fakeIPCache) GetInstanceIP(host string) (ip string) {
return f(host)
}
type podCacheTestConfig struct {
ipFunc func(string) string // Construct will set a default if nil
nodes []api.Node
pods []api.Pod
kubeletContainerInfo api.PodInfo
// Construct will fill in these fields
fakePodInfo *FakePodInfoGetter
fakeNodes *client.Fake
fakePods *registrytest.PodRegistry
}
func (c *podCacheTestConfig) Construct() *PodCache {
if c.ipFunc == nil {
c.ipFunc = func(host string) string {
return "ip of " + host
}
}
c.fakePodInfo = &FakePodInfoGetter{
data: api.PodContainerInfo{
ContainerInfo: c.kubeletContainerInfo,
},
}
fake := FakePodInfoGetter{
data: expected,
c.fakeNodes = &client.Fake{
MinionsList: api.NodeList{
Items: c.nodes,
},
}
cache := NewPodCache(&fake, nil)
c.fakePods = registrytest.NewPodRegistry(&api.PodList{Items: c.pods})
return NewPodCache(
fakeIPCache(c.ipFunc),
c.fakePodInfo,
c.fakeNodes.Nodes(),
c.fakePods,
)
}
cache.updatePodInfo("host", api.NamespaceDefault, "foo")
if fake.host != "host" || fake.id != "foo" || fake.namespace != api.NamespaceDefault {
t.Errorf("Unexpected access: %#v", fake)
func makePod(namespace, name, host string, containers ...string) *api.Pod {
pod := &api.Pod{
ObjectMeta: api.ObjectMeta{Namespace: namespace, Name: name},
Status: api.PodStatus{Host: host},
}
info, err := cache.GetPodInfo("host", api.NamespaceDefault, "foo")
if err != nil {
t.Errorf("Unexpected error: %#v", err)
for _, c := range containers {
pod.Spec.Containers = append(pod.Spec.Containers, api.Container{
Name: c,
})
}
if !reflect.DeepEqual(info, expected) {
t.Errorf("Unexpected mismatch. Expected: %#v, Got: #%v", &expected, info)
return pod
}
func makeNode(name string) *api.Node {
return &api.Node{
ObjectMeta: api.ObjectMeta{Name: name},
}
}
func TestPodUpdateAllContainers(t *testing.T) {
pod := api.Pod{
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault},
Status: api.PodStatus{
Host: "machine",
pod := makePod(api.NamespaceDefault, "foo", "machine", "bar")
config := podCacheTestConfig{
ipFunc: func(host string) string {
if host == "machine" {
return "1.2.3.5"
}
return ""
},
kubeletContainerInfo: api.PodInfo{"bar": api.ContainerStatus{}},
nodes: []api.Node{*makeNode("machine")},
pods: []api.Pod{*pod},
}
pods := []api.Pod{pod}
mockRegistry := registrytest.NewPodRegistry(&api.PodList{Items: pods})
expected := api.PodContainerInfo{
ContainerInfo: api.PodInfo{
"foo": api.ContainerStatus{},
},
}
fake := FakePodInfoGetter{
data: expected,
}
cache := NewPodCache(&fake, mockRegistry)
cache := config.Construct()
cache.UpdateAllContainers()
fake := config.fakePodInfo
if fake.host != "machine" || fake.id != "foo" || fake.namespace != api.NamespaceDefault {
t.Errorf("Unexpected access: %#v", fake)
t.Errorf("Unexpected access: %+v", fake)
}
info, err := cache.GetPodInfo("machine", api.NamespaceDefault, "foo")
status, err := cache.GetPodStatus(api.NamespaceDefault, "foo")
if err != nil {
t.Errorf("Unexpected error: %#v", err)
t.Fatalf("Unexpected error: %+v", err)
}
if !reflect.DeepEqual(info, expected) {
t.Errorf("Unexpected mismatch. Expected: %#v, Got: #%v", &expected, info)
if e, a := config.kubeletContainerInfo, status.Info; !reflect.DeepEqual(e, a) {
t.Errorf("Unexpected mismatch. Expected: %+v, Got: %+v", e, a)
}
if e, a := "1.2.3.5", status.HostIP; e != a {
t.Errorf("Unexpected mismatch. Expected: %+v, Got: %+v", e, a)
}
}
func TestFillPodStatusNoHost(t *testing.T) {
pod := makePod(api.NamespaceDefault, "foo", "", "bar")
config := podCacheTestConfig{
kubeletContainerInfo: api.PodInfo{},
nodes: []api.Node{*makeNode("machine")},
pods: []api.Pod{*pod},
}
cache := config.Construct()
err := cache.updatePodStatus(&config.pods[0])
if err != nil {
t.Fatalf("Unexpected error: %+v", err)
}
status, err := cache.GetPodStatus(pod.Namespace, pod.Name)
if e, a := api.PodPending, status.Phase; e != a {
t.Errorf("Expected: %+v, Got %+v", e, a)
}
}
func TestFillPodStatusMissingMachine(t *testing.T) {
pod := makePod(api.NamespaceDefault, "foo", "machine", "bar")
config := podCacheTestConfig{
kubeletContainerInfo: api.PodInfo{},
nodes: []api.Node{},
pods: []api.Pod{*pod},
}
cache := config.Construct()
err := cache.updatePodStatus(&config.pods[0])
if err != nil {
t.Fatalf("Unexpected error: %+v", err)
}
status, err := cache.GetPodStatus(pod.Namespace, pod.Name)
if e, a := api.PodFailed, status.Phase; e != a {
t.Errorf("Expected: %+v, Got %+v", e, a)
}
}
func TestFillPodStatus(t *testing.T) {
pod := makePod(api.NamespaceDefault, "foo", "machine", "bar")
expectedIP := "1.2.3.4"
expectedTime, _ := time.Parse("2013-Feb-03", "2013-Feb-03")
config := podCacheTestConfig{
kubeletContainerInfo: api.PodInfo{
"net": {
State: api.ContainerState{
Running: &api.ContainerStateRunning{
StartedAt: util.NewTime(expectedTime),
},
},
RestartCount: 1,
PodIP: expectedIP,
},
},
nodes: []api.Node{*makeNode("machine")},
pods: []api.Pod{*pod},
}
cache := config.Construct()
err := cache.updatePodStatus(&config.pods[0])
if err != nil {
t.Fatalf("Unexpected error: %+v", err)
}
status, err := cache.GetPodStatus(pod.Namespace, pod.Name)
if e, a := config.kubeletContainerInfo, status.Info; !reflect.DeepEqual(e, a) {
t.Errorf("Expected: %+v, Got %+v", e, a)
}
if status.PodIP != expectedIP {
t.Errorf("Expected %s, Got %s\n%+v", expectedIP, status.PodIP, status)
}
}
func TestFillPodInfoNoData(t *testing.T) {
pod := makePod(api.NamespaceDefault, "foo", "machine", "bar")
expectedIP := ""
config := podCacheTestConfig{
kubeletContainerInfo: api.PodInfo{
"net": {},
},
nodes: []api.Node{*makeNode("machine")},
pods: []api.Pod{*pod},
}
cache := config.Construct()
err := cache.updatePodStatus(&config.pods[0])
if err != nil {
t.Fatalf("Unexpected error: %+v", err)
}
status, err := cache.GetPodStatus(pod.Namespace, pod.Name)
if e, a := config.kubeletContainerInfo, status.Info; !reflect.DeepEqual(e, a) {
t.Errorf("Expected: %+v, Got %+v", e, a)
}
if status.PodIP != expectedIP {
t.Errorf("Expected %s, Got %s", expectedIP, status.PodIP)
}
}
func TestPodPhaseWithBadNode(t *testing.T) {
desiredState := api.PodSpec{
Containers: []api.Container{
{Name: "containerA"},
{Name: "containerB"},
},
RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}},
}
runningState := api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{},
},
}
stoppedState := api.ContainerStatus{
State: api.ContainerState{
Termination: &api.ContainerStateTerminated{},
},
}
tests := []struct {
pod *api.Pod
status api.PodPhase
test string
}{
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Host: "machine-2",
},
},
api.PodFailed,
"no info, but bad machine",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
"containerB": runningState,
},
Host: "machine-two",
},
},
api.PodFailed,
"all running but minion is missing",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": stoppedState,
"containerB": stoppedState,
},
Host: "machine-two",
},
},
api.PodFailed,
"all stopped but minion missing",
},
}
for _, test := range tests {
config := podCacheTestConfig{
kubeletContainerInfo: test.pod.Status.Info,
nodes: []api.Node{},
pods: []api.Pod{*test.pod},
}
cache := config.Construct()
cache.UpdateAllContainers()
status, err := cache.GetPodStatus(test.pod.Namespace, test.pod.Name)
if err != nil {
t.Errorf("%v: Unexpected error %v", test.test, err)
continue
}
if e, a := test.status, status.Phase; e != a {
t.Errorf("In test %s, expected %v, got %v", test.test, e, a)
}
}
}
func TestPodPhaseWithRestartAlways(t *testing.T) {
desiredState := api.PodSpec{
Containers: []api.Container{
{Name: "containerA"},
{Name: "containerB"},
},
RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}},
}
currentState := api.PodStatus{
Host: "machine",
}
runningState := api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{},
},
}
stoppedState := api.ContainerStatus{
State: api.ContainerState{
Termination: &api.ContainerStateTerminated{},
},
}
tests := []struct {
pod *api.Pod
status api.PodPhase
test string
}{
{&api.Pod{Spec: desiredState, Status: currentState}, api.PodPending, "waiting"},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
"containerB": runningState,
},
Host: "machine",
},
},
api.PodRunning,
"all running",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": stoppedState,
"containerB": stoppedState,
},
Host: "machine",
},
},
api.PodRunning,
"all stopped with restart always",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
"containerB": stoppedState,
},
Host: "machine",
},
},
api.PodRunning,
"mixed state #1 with restart always",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
},
Host: "machine",
},
},
api.PodPending,
"mixed state #2 with restart always",
},
}
for _, test := range tests {
if status := getPhase(&test.pod.Spec, test.pod.Status.Info); status != test.status {
t.Errorf("In test %s, expected %v, got %v", test.test, test.status, status)
}
}
}
func TestPodPhaseWithRestartNever(t *testing.T) {
desiredState := api.PodSpec{
Containers: []api.Container{
{Name: "containerA"},
{Name: "containerB"},
},
RestartPolicy: api.RestartPolicy{Never: &api.RestartPolicyNever{}},
}
currentState := api.PodStatus{
Host: "machine",
}
runningState := api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{},
},
}
succeededState := api.ContainerStatus{
State: api.ContainerState{
Termination: &api.ContainerStateTerminated{
ExitCode: 0,
},
},
}
failedState := api.ContainerStatus{
State: api.ContainerState{
Termination: &api.ContainerStateTerminated{
ExitCode: -1,
},
},
}
tests := []struct {
pod *api.Pod
status api.PodPhase
test string
}{
{&api.Pod{Spec: desiredState, Status: currentState}, api.PodPending, "waiting"},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
"containerB": runningState,
},
Host: "machine",
},
},
api.PodRunning,
"all running with restart never",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": succeededState,
"containerB": succeededState,
},
Host: "machine",
},
},
api.PodSucceeded,
"all succeeded with restart never",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": failedState,
"containerB": failedState,
},
Host: "machine",
},
},
api.PodFailed,
"all failed with restart never",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
"containerB": succeededState,
},
Host: "machine",
},
},
api.PodRunning,
"mixed state #1 with restart never",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
},
Host: "machine",
},
},
api.PodPending,
"mixed state #2 with restart never",
},
}
for _, test := range tests {
if status := getPhase(&test.pod.Spec, test.pod.Status.Info); status != test.status {
t.Errorf("In test %s, expected %v, got %v", test.test, test.status, status)
}
}
}
func TestPodPhaseWithRestartOnFailure(t *testing.T) {
desiredState := api.PodSpec{
Containers: []api.Container{
{Name: "containerA"},
{Name: "containerB"},
},
RestartPolicy: api.RestartPolicy{OnFailure: &api.RestartPolicyOnFailure{}},
}
currentState := api.PodStatus{
Host: "machine",
}
runningState := api.ContainerStatus{
State: api.ContainerState{
Running: &api.ContainerStateRunning{},
},
}
succeededState := api.ContainerStatus{
State: api.ContainerState{
Termination: &api.ContainerStateTerminated{
ExitCode: 0,
},
},
}
failedState := api.ContainerStatus{
State: api.ContainerState{
Termination: &api.ContainerStateTerminated{
ExitCode: -1,
},
},
}
tests := []struct {
pod *api.Pod
status api.PodPhase
test string
}{
{&api.Pod{Spec: desiredState, Status: currentState}, api.PodPending, "waiting"},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
"containerB": runningState,
},
Host: "machine",
},
},
api.PodRunning,
"all running with restart onfailure",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": succeededState,
"containerB": succeededState,
},
Host: "machine",
},
},
api.PodSucceeded,
"all succeeded with restart onfailure",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": failedState,
"containerB": failedState,
},
Host: "machine",
},
},
api.PodRunning,
"all failed with restart never",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
"containerB": succeededState,
},
Host: "machine",
},
},
api.PodRunning,
"mixed state #1 with restart onfailure",
},
{
&api.Pod{
Spec: desiredState,
Status: api.PodStatus{
Info: map[string]api.ContainerStatus{
"containerA": runningState,
},
Host: "machine",
},
},
api.PodPending,
"mixed state #2 with restart onfailure",
},
}
for _, test := range tests {
if status := getPhase(&test.pod.Spec, test.pod.Status.Info); status != test.status {
t.Errorf("In test %s, expected %v, got %v", test.test, test.status, status)
}
}
}