disruption.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package apps
  14. import (
  15. "fmt"
  16. "time"
  17. "github.com/onsi/ginkgo"
  18. "github.com/onsi/gomega"
  19. apps "k8s.io/api/apps/v1"
  20. "k8s.io/api/core/v1"
  21. policy "k8s.io/api/policy/v1beta1"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/util/intstr"
  24. "k8s.io/apimachinery/pkg/util/wait"
  25. "k8s.io/client-go/kubernetes"
  26. "k8s.io/client-go/util/retry"
  27. "k8s.io/kubernetes/test/e2e/framework"
  28. e2elog "k8s.io/kubernetes/test/e2e/framework/log"
  29. imageutils "k8s.io/kubernetes/test/utils/image"
  30. )
  31. // schedulingTimeout is longer specifically because sometimes we need to wait
  32. // awhile to guarantee that we've been patient waiting for something ordinary
  33. // to happen: a pod to get scheduled and move into Ready
  34. const (
  35. bigClusterSize = 7
  36. schedulingTimeout = 10 * time.Minute
  37. timeout = 60 * time.Second
  38. )
  39. var _ = SIGDescribe("DisruptionController", func() {
  40. f := framework.NewDefaultFramework("disruption")
  41. var ns string
  42. var cs kubernetes.Interface
  43. ginkgo.BeforeEach(func() {
  44. cs = f.ClientSet
  45. ns = f.Namespace.Name
  46. })
  47. ginkgo.It("should create a PodDisruptionBudget", func() {
  48. createPDBMinAvailableOrDie(cs, ns, intstr.FromString("1%"))
  49. })
  50. ginkgo.It("should update PodDisruptionBudget status", func() {
  51. createPDBMinAvailableOrDie(cs, ns, intstr.FromInt(2))
  52. createPodsOrDie(cs, ns, 3)
  53. waitForPodsOrDie(cs, ns, 3)
  54. // Since disruptionAllowed starts out 0, if we see it ever become positive,
  55. // that means the controller is working.
  56. err := wait.PollImmediate(framework.Poll, timeout, func() (bool, error) {
  57. pdb, err := cs.PolicyV1beta1().PodDisruptionBudgets(ns).Get("foo", metav1.GetOptions{})
  58. if err != nil {
  59. return false, err
  60. }
  61. return pdb.Status.PodDisruptionsAllowed > 0, nil
  62. })
  63. framework.ExpectNoError(err)
  64. })
  65. evictionCases := []struct {
  66. description string
  67. minAvailable intstr.IntOrString
  68. maxUnavailable intstr.IntOrString
  69. podCount int
  70. replicaSetSize int32
  71. shouldDeny bool
  72. exclusive bool
  73. skipForBigClusters bool
  74. }{
  75. {
  76. description: "no PDB",
  77. minAvailable: intstr.FromString(""),
  78. maxUnavailable: intstr.FromString(""),
  79. podCount: 1,
  80. shouldDeny: false,
  81. }, {
  82. description: "too few pods, absolute",
  83. minAvailable: intstr.FromInt(2),
  84. maxUnavailable: intstr.FromString(""),
  85. podCount: 2,
  86. shouldDeny: true,
  87. }, {
  88. description: "enough pods, absolute",
  89. minAvailable: intstr.FromInt(2),
  90. maxUnavailable: intstr.FromString(""),
  91. podCount: 3,
  92. shouldDeny: false,
  93. }, {
  94. description: "enough pods, replicaSet, percentage",
  95. minAvailable: intstr.FromString("90%"),
  96. maxUnavailable: intstr.FromString(""),
  97. replicaSetSize: 10,
  98. exclusive: false,
  99. shouldDeny: false,
  100. }, {
  101. description: "too few pods, replicaSet, percentage",
  102. minAvailable: intstr.FromString("90%"),
  103. maxUnavailable: intstr.FromString(""),
  104. replicaSetSize: 10,
  105. exclusive: true,
  106. shouldDeny: true,
  107. // This tests assumes that there is less than replicaSetSize nodes in the cluster.
  108. skipForBigClusters: true,
  109. },
  110. {
  111. description: "maxUnavailable allow single eviction, percentage",
  112. minAvailable: intstr.FromString(""),
  113. maxUnavailable: intstr.FromString("10%"),
  114. replicaSetSize: 10,
  115. exclusive: false,
  116. shouldDeny: false,
  117. },
  118. {
  119. description: "maxUnavailable deny evictions, integer",
  120. minAvailable: intstr.FromString(""),
  121. maxUnavailable: intstr.FromInt(1),
  122. replicaSetSize: 10,
  123. exclusive: true,
  124. shouldDeny: true,
  125. // This tests assumes that there is less than replicaSetSize nodes in the cluster.
  126. skipForBigClusters: true,
  127. },
  128. }
  129. for i := range evictionCases {
  130. c := evictionCases[i]
  131. expectation := "should allow an eviction"
  132. if c.shouldDeny {
  133. expectation = "should not allow an eviction"
  134. }
  135. ginkgo.It(fmt.Sprintf("evictions: %s => %s", c.description, expectation), func() {
  136. if c.skipForBigClusters {
  137. framework.SkipUnlessNodeCountIsAtMost(bigClusterSize - 1)
  138. }
  139. createPodsOrDie(cs, ns, c.podCount)
  140. if c.replicaSetSize > 0 {
  141. createReplicaSetOrDie(cs, ns, c.replicaSetSize, c.exclusive)
  142. }
  143. if c.minAvailable.String() != "" {
  144. createPDBMinAvailableOrDie(cs, ns, c.minAvailable)
  145. }
  146. if c.maxUnavailable.String() != "" {
  147. createPDBMaxUnavailableOrDie(cs, ns, c.maxUnavailable)
  148. }
  149. // Locate a running pod.
  150. pod, err := locateRunningPod(cs, ns)
  151. framework.ExpectNoError(err)
  152. e := &policy.Eviction{
  153. ObjectMeta: metav1.ObjectMeta{
  154. Name: pod.Name,
  155. Namespace: ns,
  156. },
  157. }
  158. if c.shouldDeny {
  159. err = cs.CoreV1().Pods(ns).Evict(e)
  160. gomega.Expect(err).Should(gomega.MatchError("Cannot evict pod as it would violate the pod's disruption budget."))
  161. } else {
  162. // Only wait for running pods in the "allow" case
  163. // because one of shouldDeny cases relies on the
  164. // replicaSet not fitting on the cluster.
  165. waitForPodsOrDie(cs, ns, c.podCount+int(c.replicaSetSize))
  166. // Since disruptionAllowed starts out false, if an eviction is ever allowed,
  167. // that means the controller is working.
  168. err = wait.PollImmediate(framework.Poll, timeout, func() (bool, error) {
  169. err = cs.CoreV1().Pods(ns).Evict(e)
  170. if err != nil {
  171. return false, nil
  172. }
  173. return true, nil
  174. })
  175. framework.ExpectNoError(err)
  176. }
  177. })
  178. }
  179. ginkgo.It("should block an eviction until the PDB is updated to allow it", func() {
  180. ginkgo.By("Creating a pdb that targets all three pods in a test replica set")
  181. createPDBMinAvailableOrDie(cs, ns, intstr.FromInt(3))
  182. createReplicaSetOrDie(cs, ns, 3, false)
  183. ginkgo.By("First trying to evict a pod which shouldn't be evictable")
  184. pod, err := locateRunningPod(cs, ns)
  185. framework.ExpectNoError(err)
  186. waitForPodsOrDie(cs, ns, 3) // make sure that they are running and so would be evictable with a different pdb
  187. e := &policy.Eviction{
  188. ObjectMeta: metav1.ObjectMeta{
  189. Name: pod.Name,
  190. Namespace: ns,
  191. },
  192. }
  193. err = cs.CoreV1().Pods(ns).Evict(e)
  194. gomega.Expect(err).Should(gomega.MatchError("Cannot evict pod as it would violate the pod's disruption budget."))
  195. ginkgo.By("Updating the pdb to allow a pod to be evicted")
  196. updatePDBMinAvailableOrDie(cs, ns, intstr.FromInt(2))
  197. ginkgo.By("Trying to evict the same pod we tried earlier which should now be evictable")
  198. waitForPodsOrDie(cs, ns, 3)
  199. err = cs.CoreV1().Pods(ns).Evict(e)
  200. framework.ExpectNoError(err) // the eviction is now allowed
  201. })
  202. })
  203. func createPDBMinAvailableOrDie(cs kubernetes.Interface, ns string, minAvailable intstr.IntOrString) {
  204. pdb := policy.PodDisruptionBudget{
  205. ObjectMeta: metav1.ObjectMeta{
  206. Name: "foo",
  207. Namespace: ns,
  208. },
  209. Spec: policy.PodDisruptionBudgetSpec{
  210. Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
  211. MinAvailable: &minAvailable,
  212. },
  213. }
  214. _, err := cs.PolicyV1beta1().PodDisruptionBudgets(ns).Create(&pdb)
  215. framework.ExpectNoError(err, "Waiting for the pdb to be created with minAvailable %d in namespace %s", minAvailable.IntVal, ns)
  216. waitForPdbToBeProcessed(cs, ns)
  217. }
  218. func createPDBMaxUnavailableOrDie(cs kubernetes.Interface, ns string, maxUnavailable intstr.IntOrString) {
  219. pdb := policy.PodDisruptionBudget{
  220. ObjectMeta: metav1.ObjectMeta{
  221. Name: "foo",
  222. Namespace: ns,
  223. },
  224. Spec: policy.PodDisruptionBudgetSpec{
  225. Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
  226. MaxUnavailable: &maxUnavailable,
  227. },
  228. }
  229. _, err := cs.PolicyV1beta1().PodDisruptionBudgets(ns).Create(&pdb)
  230. framework.ExpectNoError(err, "Waiting for the pdb to be created with maxUnavailable %d in namespace %s", maxUnavailable.IntVal, ns)
  231. waitForPdbToBeProcessed(cs, ns)
  232. }
  233. func updatePDBMinAvailableOrDie(cs kubernetes.Interface, ns string, minAvailable intstr.IntOrString) {
  234. err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
  235. old, err := cs.PolicyV1beta1().PodDisruptionBudgets(ns).Get("foo", metav1.GetOptions{})
  236. if err != nil {
  237. return err
  238. }
  239. old.Spec.MinAvailable = &minAvailable
  240. if _, err := cs.PolicyV1beta1().PodDisruptionBudgets(ns).Update(old); err != nil {
  241. return err
  242. }
  243. return nil
  244. })
  245. framework.ExpectNoError(err, "Waiting for the pdb update to be processed in namespace %s", ns)
  246. waitForPdbToBeProcessed(cs, ns)
  247. }
  248. func createPodsOrDie(cs kubernetes.Interface, ns string, n int) {
  249. for i := 0; i < n; i++ {
  250. pod := &v1.Pod{
  251. ObjectMeta: metav1.ObjectMeta{
  252. Name: fmt.Sprintf("pod-%d", i),
  253. Namespace: ns,
  254. Labels: map[string]string{"foo": "bar"},
  255. },
  256. Spec: v1.PodSpec{
  257. Containers: []v1.Container{
  258. {
  259. Name: "busybox",
  260. Image: imageutils.GetE2EImage(imageutils.EchoServer),
  261. },
  262. },
  263. RestartPolicy: v1.RestartPolicyAlways,
  264. },
  265. }
  266. _, err := cs.CoreV1().Pods(ns).Create(pod)
  267. framework.ExpectNoError(err, "Creating pod %q in namespace %q", pod.Name, ns)
  268. }
  269. }
  270. func waitForPodsOrDie(cs kubernetes.Interface, ns string, n int) {
  271. ginkgo.By("Waiting for all pods to be running")
  272. err := wait.PollImmediate(framework.Poll, schedulingTimeout, func() (bool, error) {
  273. pods, err := cs.CoreV1().Pods(ns).List(metav1.ListOptions{LabelSelector: "foo=bar"})
  274. if err != nil {
  275. return false, err
  276. }
  277. if pods == nil {
  278. return false, fmt.Errorf("pods is nil")
  279. }
  280. if len(pods.Items) < n {
  281. e2elog.Logf("pods: %v < %v", len(pods.Items), n)
  282. return false, nil
  283. }
  284. ready := 0
  285. for i := 0; i < n; i++ {
  286. if pods.Items[i].Status.Phase == v1.PodRunning {
  287. ready++
  288. }
  289. }
  290. if ready < n {
  291. e2elog.Logf("running pods: %v < %v", ready, n)
  292. return false, nil
  293. }
  294. return true, nil
  295. })
  296. framework.ExpectNoError(err, "Waiting for pods in namespace %q to be ready", ns)
  297. }
  298. func createReplicaSetOrDie(cs kubernetes.Interface, ns string, size int32, exclusive bool) {
  299. container := v1.Container{
  300. Name: "busybox",
  301. Image: imageutils.GetE2EImage(imageutils.EchoServer),
  302. }
  303. if exclusive {
  304. container.Ports = []v1.ContainerPort{
  305. {HostPort: 5555, ContainerPort: 5555},
  306. }
  307. }
  308. rs := &apps.ReplicaSet{
  309. ObjectMeta: metav1.ObjectMeta{
  310. Name: "rs",
  311. Namespace: ns,
  312. },
  313. Spec: apps.ReplicaSetSpec{
  314. Replicas: &size,
  315. Selector: &metav1.LabelSelector{
  316. MatchLabels: map[string]string{"foo": "bar"},
  317. },
  318. Template: v1.PodTemplateSpec{
  319. ObjectMeta: metav1.ObjectMeta{
  320. Labels: map[string]string{"foo": "bar"},
  321. },
  322. Spec: v1.PodSpec{
  323. Containers: []v1.Container{container},
  324. },
  325. },
  326. },
  327. }
  328. _, err := cs.AppsV1().ReplicaSets(ns).Create(rs)
  329. framework.ExpectNoError(err, "Creating replica set %q in namespace %q", rs.Name, ns)
  330. }
  331. func locateRunningPod(cs kubernetes.Interface, ns string) (pod *v1.Pod, err error) {
  332. ginkgo.By("locating a running pod")
  333. err = wait.PollImmediate(framework.Poll, schedulingTimeout, func() (bool, error) {
  334. podList, err := cs.CoreV1().Pods(ns).List(metav1.ListOptions{})
  335. if err != nil {
  336. return false, err
  337. }
  338. for i := range podList.Items {
  339. if podList.Items[i].Status.Phase == v1.PodRunning {
  340. pod = &podList.Items[i]
  341. return true, nil
  342. }
  343. }
  344. return false, nil
  345. })
  346. return pod, err
  347. }
  348. func waitForPdbToBeProcessed(cs kubernetes.Interface, ns string) {
  349. ginkgo.By("Waiting for the pdb to be processed")
  350. err := wait.PollImmediate(framework.Poll, schedulingTimeout, func() (bool, error) {
  351. pdb, err := cs.PolicyV1beta1().PodDisruptionBudgets(ns).Get("foo", metav1.GetOptions{})
  352. if err != nil {
  353. return false, err
  354. }
  355. if pdb.Status.ObservedGeneration < pdb.Generation {
  356. return false, nil
  357. }
  358. return true, nil
  359. })
  360. framework.ExpectNoError(err, "Waiting for the pdb to be processed in namespace %s", ns)
  361. }