-
Notifications
You must be signed in to change notification settings - Fork 38.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add e2e test for network partition #19731
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,13 +33,21 @@ import ( | |
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
"k8s.io/kubernetes/pkg/client/cache" | ||
awscloud "k8s.io/kubernetes/pkg/cloudprovider/providers/aws" | ||
controllerframework "k8s.io/kubernetes/pkg/controller/framework" | ||
"k8s.io/kubernetes/pkg/fields" | ||
"k8s.io/kubernetes/pkg/runtime" | ||
"k8s.io/kubernetes/pkg/watch" | ||
) | ||
|
||
const ( | ||
serveHostnameImage = "gcr.io/google_containers/serve_hostname:1.1" | ||
resizeNodeReadyTimeout = 2 * time.Minute | ||
resizeNodeNotReadyTimeout = 2 * time.Minute | ||
nodeReadinessTimeout = 3 * time.Minute | ||
podNotReadyTimeout = 1 * time.Minute | ||
podReadyTimeout = 2 * time.Minute | ||
testPort = 9376 | ||
) | ||
|
||
|
@@ -287,35 +295,55 @@ func verifyPods(c *client.Client, ns, name string, wantName bool, replicas int) | |
return nil | ||
} | ||
|
||
// Blocks outgoing network traffic on 'node'. Then verifies that 'podNameToDisappear', | ||
// that belongs to replication controller 'rcName', really disappeared. | ||
// Finally, it checks that the replication controller recreates the | ||
// pods on another node and that now the number of replicas is equal 'replicas'. | ||
// At the end (even in case of errors), the network traffic is brought back to normal. | ||
// This function executes commands on a node so it will work only for some | ||
// environments. | ||
func performTemporaryNetworkFailure(c *client.Client, ns, rcName string, replicas int, podNameToDisappear string, node *api.Node) { | ||
Logf("Getting external IP address for %s", node.Name) | ||
host := "" | ||
for _, a := range node.Status.Addresses { | ||
if a.Type == api.NodeExternalIP { | ||
host = a.Address + ":22" | ||
break | ||
} | ||
func blockNetwork(from string, to string) { | ||
Logf("block network traffic from %s to %s", from, to) | ||
iptablesRule := fmt.Sprintf("OUTPUT --destination %s --jump REJECT", to) | ||
dropCmd := fmt.Sprintf("sudo iptables --insert %s", iptablesRule) | ||
if result, err := SSH(dropCmd, from, testContext.Provider); result.Code != 0 || err != nil { | ||
LogSSHResult(result) | ||
Failf("Unexpected error: %v", err) | ||
} | ||
if host == "" { | ||
Failf("Couldn't get the external IP of host %s with addresses %v", node.Name, node.Status.Addresses) | ||
} | ||
|
||
func unblockNetwork(from string, to string) { | ||
Logf("Unblock network traffic from %s to %s", from, to) | ||
iptablesRule := fmt.Sprintf("OUTPUT --destination %s --jump REJECT", to) | ||
undropCmd := fmt.Sprintf("sudo iptables --delete %s", iptablesRule) | ||
// Undrop command may fail if the rule has never been created. | ||
// In such case we just lose 30 seconds, but the cluster is healthy. | ||
// But if the rule had been created and removing it failed, the node is broken and | ||
// not coming back. Subsequent tests will run or fewer nodes (some of the tests | ||
// may fail). Manual intervention is required in such case (recreating the | ||
// cluster solves the problem too). | ||
err := wait.Poll(time.Millisecond*100, time.Second*30, func() (bool, error) { | ||
result, err := SSH(undropCmd, from, testContext.Provider) | ||
if result.Code == 0 && err == nil { | ||
return true, nil | ||
} | ||
LogSSHResult(result) | ||
if err != nil { | ||
Logf("Unexpected error: %v", err) | ||
} | ||
return false, nil | ||
}) | ||
if err != nil { | ||
Failf("Failed to remove the iptable REJECT rule. Manual intervention is "+ | ||
"required on host %s: remove rule %s, if exists", from, iptablesRule) | ||
} | ||
} | ||
|
||
By(fmt.Sprintf("block network traffic from node %s to the master", node.Name)) | ||
func getMaster(c *client.Client) string { | ||
master := "" | ||
switch testContext.Provider { | ||
case "gce": | ||
// TODO(#10085): The use of MasterName will cause iptables to do a DNS | ||
// lookup to resolve the name to an IP address, which will slow down the | ||
// test and cause it to fail if DNS is absent or broken. Use the | ||
// internal IP address instead (i.e. NOT the one in testContext.Host). | ||
master = testContext.CloudConfig.MasterName | ||
eps, err := c.Endpoints(api.NamespaceDefault).Get("kubernetes") | ||
if err != nil { | ||
Failf("Fail to get kubernetes endpoinds: %v", err) | ||
} | ||
if len(eps.Subsets) != 1 || len(eps.Subsets[0].Addresses) != 1 { | ||
Failf("There are more than 1 endpoints for kubernetes service: %+v", eps) | ||
} | ||
master = eps.Subsets[0].Addresses[0].IP | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the IP instead of host name... This is to avoid external DNS dependancy for iptables. |
||
case "gke": | ||
master = strings.TrimPrefix(testContext.Host, "https://") | ||
case "aws": | ||
|
@@ -324,53 +352,51 @@ func performTemporaryNetworkFailure(c *client.Client, ns, rcName string, replica | |
default: | ||
Failf("This test is not supported for provider %s and should be disabled", testContext.Provider) | ||
} | ||
iptablesRule := fmt.Sprintf("OUTPUT --destination %s --jump REJECT", master) | ||
return master | ||
} | ||
|
||
// Return node external IP concatenated with port 22 for ssh | ||
// e.g. 1.2.3.4:22 | ||
func getNodeExternalIP(node *api.Node) string { | ||
Logf("Getting external IP address for %s", node.Name) | ||
host := "" | ||
for _, a := range node.Status.Addresses { | ||
if a.Type == api.NodeExternalIP { | ||
host = a.Address + ":22" | ||
break | ||
} | ||
} | ||
if host == "" { | ||
Failf("Couldn't get the external IP of host %s with addresses %v", node.Name, node.Status.Addresses) | ||
} | ||
return host | ||
} | ||
|
||
// Blocks outgoing network traffic on 'node'. Then verifies that 'podNameToDisappear', | ||
// that belongs to replication controller 'rcName', really disappeared. | ||
// Finally, it checks that the replication controller recreates the | ||
// pods on another node and that now the number of replicas is equal 'replicas'. | ||
// At the end (even in case of errors), the network traffic is brought back to normal. | ||
// This function executes commands on a node so it will work only for some | ||
// environments. | ||
func performTemporaryNetworkFailure(c *client.Client, ns, rcName string, replicas int, podNameToDisappear string, node *api.Node) { | ||
host := getNodeExternalIP(node) | ||
master := getMaster(c) | ||
By(fmt.Sprintf("block network traffic from node %s to the master", node.Name)) | ||
defer func() { | ||
// This code will execute even if setting the iptables rule failed. | ||
// It is on purpose because we may have an error even if the new rule | ||
// had been inserted. (yes, we could look at the error code and ssh error | ||
// separately, but I prefer to stay on the safe side). | ||
|
||
By(fmt.Sprintf("Unblock network traffic from node %s to the master", node.Name)) | ||
undropCmd := fmt.Sprintf("sudo iptables --delete %s", iptablesRule) | ||
// Undrop command may fail if the rule has never been created. | ||
// In such case we just lose 30 seconds, but the cluster is healthy. | ||
// But if the rule had been created and removing it failed, the node is broken and | ||
// not coming back. Subsequent tests will run or fewer nodes (some of the tests | ||
// may fail). Manual intervention is required in such case (recreating the | ||
// cluster solves the problem too). | ||
err := wait.Poll(time.Millisecond*100, time.Second*30, func() (bool, error) { | ||
result, err := SSH(undropCmd, host, testContext.Provider) | ||
if result.Code == 0 && err == nil { | ||
return true, nil | ||
} | ||
LogSSHResult(result) | ||
if err != nil { | ||
Logf("Unexpected error: %v", err) | ||
} | ||
return false, nil | ||
}) | ||
if err != nil { | ||
Failf("Failed to remove the iptable REJECT rule. Manual intervention is "+ | ||
"required on node %s: remove rule %s, if exists", node.Name, iptablesRule) | ||
} | ||
unblockNetwork(host, master) | ||
}() | ||
|
||
Logf("Waiting %v to ensure node %s is ready before beginning test...", resizeNodeReadyTimeout, node.Name) | ||
if !waitForNodeToBe(c, node.Name, api.NodeReady, true, resizeNodeReadyTimeout) { | ||
Failf("Node %s did not become ready within %v", node.Name, resizeNodeReadyTimeout) | ||
} | ||
|
||
// The command will block all outgoing network traffic from the node to the master | ||
// When multi-master is implemented, this test will have to be improved to block | ||
// network traffic to all masters. | ||
// We could also block network traffic from the master(s) to this node, | ||
// but blocking it one way is sufficient for this test. | ||
dropCmd := fmt.Sprintf("sudo iptables --insert %s", iptablesRule) | ||
if result, err := SSH(dropCmd, host, testContext.Provider); result.Code != 0 || err != nil { | ||
LogSSHResult(result) | ||
Failf("Unexpected error: %v", err) | ||
} | ||
blockNetwork(host, master) | ||
|
||
Logf("Waiting %v for node %s to be not ready after simulated network failure", resizeNodeNotReadyTimeout, node.Name) | ||
if !waitForNodeToBe(c, node.Name, api.NodeReady, false, resizeNodeNotReadyTimeout) { | ||
|
@@ -388,12 +414,32 @@ func performTemporaryNetworkFailure(c *client.Client, ns, rcName string, replica | |
// network traffic is unblocked in a deferred function | ||
} | ||
|
||
func expectNodeReadiness(isReady bool, newNode chan *api.Node) { | ||
timeout := false | ||
expected := false | ||
timer := time.After(nodeReadinessTimeout) | ||
for !expected && !timeout { | ||
select { | ||
case n := <-newNode: | ||
if isNodeConditionSetAsExpected(n, api.NodeReady, isReady) { | ||
expected = true | ||
} else { | ||
Logf("Observed node ready status is NOT %v as expected", isReady) | ||
} | ||
case <-timer: | ||
timeout = true | ||
} | ||
} | ||
if !expected { | ||
Failf("Failed to observe node ready status change to %v", isReady) | ||
} | ||
} | ||
|
||
var _ = Describe("Nodes [Disruptive]", func() { | ||
framework := NewFramework("resize-nodes") | ||
var systemPodsNo int | ||
var c *client.Client | ||
var ns string | ||
|
||
BeforeEach(func() { | ||
c = framework.Client | ||
ns = framework.Namespace.Name | ||
|
@@ -560,6 +606,101 @@ var _ = Describe("Nodes [Disruptive]", func() { | |
} | ||
} | ||
}) | ||
|
||
// What happens in this test: | ||
// Network traffic from a node to master is cut off to simulate network partition | ||
// Expect to observe: | ||
// 1. Node is marked NotReady after timeout by nodecontroller (40seconds) | ||
// 2. All pods on node are marked NotReady shortly after #1 | ||
// 3. Node and pods return to Ready after connectivivty recovers | ||
It("All pods on the unreachable node should be marked as NotReady upon the node turn NotReady "+ | ||
"AND all pods should be mark back to Ready when the node get back to Ready before pod eviction timeout", func() { | ||
By("choose a node - we will block all network traffic on this node") | ||
var podOpts api.ListOptions | ||
nodeOpts := api.ListOptions{} | ||
nodes, err := c.Nodes().List(nodeOpts) | ||
Expect(err).NotTo(HaveOccurred()) | ||
filterNodes(nodes, func(node api.Node) bool { | ||
if !isNodeConditionSetAsExpected(&node, api.NodeReady, true) { | ||
return false | ||
} | ||
podOpts = api.ListOptions{FieldSelector: fields.OneTermEqualSelector(client.PodHost, node.Name)} | ||
pods, err := c.Pods(api.NamespaceAll).List(podOpts) | ||
if err != nil || len(pods.Items) <= 0 { | ||
return false | ||
} | ||
return true | ||
}) | ||
if len(nodes.Items) <= 0 { | ||
Failf("No eligible node were found: %d", len(nodes.Items)) | ||
} | ||
node := nodes.Items[0] | ||
podOpts = api.ListOptions{FieldSelector: fields.OneTermEqualSelector(client.PodHost, node.Name)} | ||
if err = waitForMatchPodsCondition(c, podOpts, "Running and Ready", podReadyTimeout, podRunningReady); err != nil { | ||
Failf("Pods on node %s are not ready and running within %v: %v", node.Name, podReadyTimeout, err) | ||
} | ||
|
||
By("Set up watch on node status") | ||
nodeSelector := fields.OneTermEqualSelector("metadata.name", node.Name) | ||
stopCh := make(chan struct{}) | ||
newNode := make(chan *api.Node) | ||
var controller *controllerframework.Controller | ||
_, controller = controllerframework.NewInformer( | ||
&cache.ListWatch{ | ||
ListFunc: func(options api.ListOptions) (runtime.Object, error) { | ||
options.FieldSelector = nodeSelector | ||
return framework.Client.Nodes().List(options) | ||
}, | ||
WatchFunc: func(options api.ListOptions) (watch.Interface, error) { | ||
options.FieldSelector = nodeSelector | ||
return framework.Client.Nodes().Watch(options) | ||
}, | ||
}, | ||
&api.Node{}, | ||
0, | ||
controllerframework.ResourceEventHandlerFuncs{ | ||
UpdateFunc: func(oldObj, newObj interface{}) { | ||
n, ok := newObj.(*api.Node) | ||
Expect(ok).To(Equal(true)) | ||
newNode <- n | ||
|
||
}, | ||
}, | ||
) | ||
|
||
defer func() { | ||
// Will not explicitly close newNode channel here due to | ||
// race condition where stopCh and newNode are closed but informer onUpdate still executes. | ||
close(stopCh) | ||
}() | ||
go controller.Run(stopCh) | ||
|
||
By(fmt.Sprintf("Block traffic from node %s to the master", node.Name)) | ||
host := getNodeExternalIP(&node) | ||
master := getMaster(c) | ||
defer func() { | ||
By(fmt.Sprintf("Unblock traffic from node %s to the master", node.Name)) | ||
unblockNetwork(host, master) | ||
|
||
if CurrentGinkgoTestDescription().Failed { | ||
return | ||
} | ||
|
||
By("Expect to observe node and pod status change from NotReady to Ready after network connectivity recovers") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The section between this By and the next feels like it can be isolated into a function (in one case it's notready->ready and the other its ready->notready). Is that easy? |
||
expectNodeReadiness(true, newNode) | ||
if err = waitForMatchPodsCondition(c, podOpts, "Running and Ready", podReadyTimeout, podRunningReady); err != nil { | ||
Failf("Pods on node %s did not become ready and running within %v: %v", node.Name, podReadyTimeout, err) | ||
} | ||
}() | ||
|
||
blockNetwork(host, master) | ||
|
||
By("Expect to observe node and pod status change from Ready to NotReady after network partition") | ||
expectNodeReadiness(false, newNode) | ||
if err = waitForMatchPodsCondition(c, podOpts, "NotReady", podNotReadyTimeout, podNotReady); err != nil { | ||
Failf("Pods on node %s did not become NotReady within %v: %v", node.Name, podNotReadyTimeout, err) | ||
} | ||
}) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move the block and unblock functions into e2e/utils?
I'm alright with a follow up pr that does this, I gather many tests can use this functionality (I have one in mind already).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do a follow up PR. No need to make this one too complex.