namespaced_resources_deleter_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /*
  2. Copyright 2015 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 deletion
  14. import (
  15. "fmt"
  16. "net/http"
  17. "net/http/httptest"
  18. "path"
  19. "strings"
  20. "sync"
  21. "testing"
  22. v1 "k8s.io/api/core/v1"
  23. "k8s.io/apimachinery/pkg/api/errors"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/runtime"
  26. "k8s.io/apimachinery/pkg/runtime/schema"
  27. "k8s.io/apimachinery/pkg/util/sets"
  28. "k8s.io/client-go/discovery"
  29. "k8s.io/client-go/dynamic"
  30. "k8s.io/client-go/kubernetes/fake"
  31. "k8s.io/client-go/metadata"
  32. restclient "k8s.io/client-go/rest"
  33. core "k8s.io/client-go/testing"
  34. api "k8s.io/kubernetes/pkg/apis/core"
  35. )
  36. func TestFinalized(t *testing.T) {
  37. testNamespace := &v1.Namespace{
  38. Spec: v1.NamespaceSpec{
  39. Finalizers: []v1.FinalizerName{"a", "b"},
  40. },
  41. }
  42. if finalized(testNamespace) {
  43. t.Errorf("Unexpected result, namespace is not finalized")
  44. }
  45. testNamespace.Spec.Finalizers = []v1.FinalizerName{}
  46. if !finalized(testNamespace) {
  47. t.Errorf("Expected object to be finalized")
  48. }
  49. }
  50. func TestFinalizeNamespaceFunc(t *testing.T) {
  51. mockClient := &fake.Clientset{}
  52. testNamespace := &v1.Namespace{
  53. ObjectMeta: metav1.ObjectMeta{
  54. Name: "test",
  55. ResourceVersion: "1",
  56. },
  57. Spec: v1.NamespaceSpec{
  58. Finalizers: []v1.FinalizerName{"kubernetes", "other"},
  59. },
  60. }
  61. d := namespacedResourcesDeleter{
  62. nsClient: mockClient.CoreV1().Namespaces(),
  63. finalizerToken: v1.FinalizerKubernetes,
  64. }
  65. d.finalizeNamespace(testNamespace)
  66. actions := mockClient.Actions()
  67. if len(actions) != 1 {
  68. t.Errorf("Expected 1 mock client action, but got %v", len(actions))
  69. }
  70. if !actions[0].Matches("create", "namespaces") || actions[0].GetSubresource() != "finalize" {
  71. t.Errorf("Expected finalize-namespace action %v", actions[0])
  72. }
  73. finalizers := actions[0].(core.CreateAction).GetObject().(*v1.Namespace).Spec.Finalizers
  74. if len(finalizers) != 1 {
  75. t.Errorf("There should be a single finalizer remaining")
  76. }
  77. if string(finalizers[0]) != "other" {
  78. t.Errorf("Unexpected finalizer value, %v", finalizers[0])
  79. }
  80. }
  81. func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersions) {
  82. now := metav1.Now()
  83. namespaceName := "test"
  84. testNamespacePendingFinalize := &v1.Namespace{
  85. ObjectMeta: metav1.ObjectMeta{
  86. Name: namespaceName,
  87. ResourceVersion: "1",
  88. DeletionTimestamp: &now,
  89. },
  90. Spec: v1.NamespaceSpec{
  91. Finalizers: []v1.FinalizerName{"kubernetes"},
  92. },
  93. Status: v1.NamespaceStatus{
  94. Phase: v1.NamespaceTerminating,
  95. },
  96. }
  97. testNamespaceFinalizeComplete := &v1.Namespace{
  98. ObjectMeta: metav1.ObjectMeta{
  99. Name: namespaceName,
  100. ResourceVersion: "1",
  101. DeletionTimestamp: &now,
  102. },
  103. Spec: v1.NamespaceSpec{},
  104. Status: v1.NamespaceStatus{
  105. Phase: v1.NamespaceTerminating,
  106. },
  107. }
  108. // when doing a delete all of content, we will do a GET of a collection, and DELETE of a collection by default
  109. metadataClientActionSet := sets.NewString()
  110. resources := testResources()
  111. groupVersionResources, _ := discovery.GroupVersionResources(resources)
  112. for groupVersionResource := range groupVersionResources {
  113. urlPath := path.Join([]string{
  114. dynamic.LegacyAPIPathResolverFunc(schema.GroupVersionKind{Group: groupVersionResource.Group, Version: groupVersionResource.Version}),
  115. groupVersionResource.Group,
  116. groupVersionResource.Version,
  117. "namespaces",
  118. namespaceName,
  119. groupVersionResource.Resource,
  120. }...)
  121. metadataClientActionSet.Insert((&fakeAction{method: "GET", path: urlPath}).String())
  122. metadataClientActionSet.Insert((&fakeAction{method: "DELETE", path: urlPath}).String())
  123. }
  124. scenarios := map[string]struct {
  125. testNamespace *v1.Namespace
  126. kubeClientActionSet sets.String
  127. metadataClientActionSet sets.String
  128. gvrError error
  129. expectErrorOnDelete error
  130. expectStatus *v1.NamespaceStatus
  131. }{
  132. "pending-finalize": {
  133. testNamespace: testNamespacePendingFinalize,
  134. kubeClientActionSet: sets.NewString(
  135. strings.Join([]string{"get", "namespaces", ""}, "-"),
  136. strings.Join([]string{"create", "namespaces", "finalize"}, "-"),
  137. strings.Join([]string{"list", "pods", ""}, "-"),
  138. strings.Join([]string{"update", "namespaces", "status"}, "-"),
  139. ),
  140. metadataClientActionSet: metadataClientActionSet,
  141. },
  142. "complete-finalize": {
  143. testNamespace: testNamespaceFinalizeComplete,
  144. kubeClientActionSet: sets.NewString(
  145. strings.Join([]string{"get", "namespaces", ""}, "-"),
  146. ),
  147. metadataClientActionSet: sets.NewString(),
  148. },
  149. "groupVersionResourceErr": {
  150. testNamespace: testNamespaceFinalizeComplete,
  151. kubeClientActionSet: sets.NewString(
  152. strings.Join([]string{"get", "namespaces", ""}, "-"),
  153. ),
  154. metadataClientActionSet: sets.NewString(),
  155. gvrError: fmt.Errorf("test error"),
  156. },
  157. "groupVersionResourceErr-finalize": {
  158. testNamespace: testNamespacePendingFinalize,
  159. kubeClientActionSet: sets.NewString(
  160. strings.Join([]string{"get", "namespaces", ""}, "-"),
  161. strings.Join([]string{"list", "pods", ""}, "-"),
  162. strings.Join([]string{"update", "namespaces", "status"}, "-"),
  163. ),
  164. metadataClientActionSet: metadataClientActionSet,
  165. gvrError: fmt.Errorf("test error"),
  166. expectErrorOnDelete: fmt.Errorf("test error"),
  167. expectStatus: &v1.NamespaceStatus{
  168. Phase: v1.NamespaceTerminating,
  169. Conditions: []v1.NamespaceCondition{
  170. {Type: v1.NamespaceDeletionDiscoveryFailure},
  171. },
  172. },
  173. },
  174. }
  175. for scenario, testInput := range scenarios {
  176. t.Run(scenario, func(t *testing.T) {
  177. testHandler := &fakeActionHandler{statusCode: 200}
  178. srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
  179. defer srv.Close()
  180. mockClient := fake.NewSimpleClientset(testInput.testNamespace)
  181. metadataClient, err := metadata.NewForConfig(clientConfig)
  182. if err != nil {
  183. t.Fatal(err)
  184. }
  185. fn := func() ([]*metav1.APIResourceList, error) {
  186. return resources, testInput.gvrError
  187. }
  188. d := NewNamespacedResourcesDeleter(mockClient.CoreV1().Namespaces(), metadataClient, mockClient.CoreV1(), fn, v1.FinalizerKubernetes)
  189. if err := d.Delete(testInput.testNamespace.Name); !matchErrors(err, testInput.expectErrorOnDelete) {
  190. t.Errorf("expected error %q when syncing namespace, got %q, %v", testInput.expectErrorOnDelete, err, testInput.expectErrorOnDelete == err)
  191. }
  192. // validate traffic from kube client
  193. actionSet := sets.NewString()
  194. for _, action := range mockClient.Actions() {
  195. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  196. }
  197. if !actionSet.Equal(testInput.kubeClientActionSet) {
  198. t.Errorf("mock client expected actions:\n%v\n but got:\n%v\nDifference:\n%v",
  199. testInput.kubeClientActionSet, actionSet, testInput.kubeClientActionSet.Difference(actionSet))
  200. }
  201. // validate traffic from metadata client
  202. actionSet = sets.NewString()
  203. for _, action := range testHandler.actions {
  204. actionSet.Insert(action.String())
  205. }
  206. if !actionSet.Equal(testInput.metadataClientActionSet) {
  207. t.Errorf(" metadata client expected actions:\n%v\n but got:\n%v\nDifference:\n%v",
  208. testInput.metadataClientActionSet, actionSet, testInput.metadataClientActionSet.Difference(actionSet))
  209. }
  210. // validate status conditions
  211. if testInput.expectStatus != nil {
  212. obj, err := mockClient.Tracker().Get(schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}, testInput.testNamespace.Namespace, testInput.testNamespace.Name)
  213. if err != nil {
  214. t.Fatalf("Unexpected error in getting the namespace: %v", err)
  215. }
  216. ns, ok := obj.(*v1.Namespace)
  217. if !ok {
  218. t.Fatalf("Expected a namespace but received %v", obj)
  219. }
  220. if ns.Status.Phase != testInput.expectStatus.Phase {
  221. t.Fatalf("Expected namespace status phase %v but received %v", testInput.expectStatus.Phase, ns.Status.Phase)
  222. }
  223. for _, expCondition := range testInput.expectStatus.Conditions {
  224. nsCondition := getCondition(ns.Status.Conditions, expCondition.Type)
  225. if nsCondition == nil {
  226. t.Fatalf("Missing namespace status condition %v", expCondition.Type)
  227. }
  228. }
  229. }
  230. })
  231. }
  232. }
  233. func TestRetryOnConflictError(t *testing.T) {
  234. mockClient := &fake.Clientset{}
  235. numTries := 0
  236. retryOnce := func(namespace *v1.Namespace) (*v1.Namespace, error) {
  237. numTries++
  238. if numTries <= 1 {
  239. return namespace, errors.NewConflict(api.Resource("namespaces"), namespace.Name, fmt.Errorf("ERROR"))
  240. }
  241. return namespace, nil
  242. }
  243. namespace := &v1.Namespace{}
  244. d := namespacedResourcesDeleter{
  245. nsClient: mockClient.CoreV1().Namespaces(),
  246. }
  247. _, err := d.retryOnConflictError(namespace, retryOnce)
  248. if err != nil {
  249. t.Errorf("Unexpected error %v", err)
  250. }
  251. if numTries != 2 {
  252. t.Errorf("Expected %v, but got %v", 2, numTries)
  253. }
  254. }
  255. func TestSyncNamespaceThatIsTerminatingNonExperimental(t *testing.T) {
  256. testSyncNamespaceThatIsTerminating(t, &metav1.APIVersions{})
  257. }
  258. func TestSyncNamespaceThatIsTerminatingV1(t *testing.T) {
  259. testSyncNamespaceThatIsTerminating(t, &metav1.APIVersions{Versions: []string{"apps/v1"}})
  260. }
  261. func TestSyncNamespaceThatIsActive(t *testing.T) {
  262. mockClient := &fake.Clientset{}
  263. testNamespace := &v1.Namespace{
  264. ObjectMeta: metav1.ObjectMeta{
  265. Name: "test",
  266. ResourceVersion: "1",
  267. },
  268. Spec: v1.NamespaceSpec{
  269. Finalizers: []v1.FinalizerName{"kubernetes"},
  270. },
  271. Status: v1.NamespaceStatus{
  272. Phase: v1.NamespaceActive,
  273. },
  274. }
  275. fn := func() ([]*metav1.APIResourceList, error) {
  276. return testResources(), nil
  277. }
  278. d := NewNamespacedResourcesDeleter(mockClient.CoreV1().Namespaces(), nil, mockClient.CoreV1(),
  279. fn, v1.FinalizerKubernetes)
  280. err := d.Delete(testNamespace.Name)
  281. if err != nil {
  282. t.Errorf("Unexpected error when synching namespace %v", err)
  283. }
  284. if len(mockClient.Actions()) != 1 {
  285. t.Errorf("Expected only one action from controller, but got: %d %v", len(mockClient.Actions()), mockClient.Actions())
  286. }
  287. action := mockClient.Actions()[0]
  288. if !action.Matches("get", "namespaces") {
  289. t.Errorf("Expected get namespaces, got: %v", action)
  290. }
  291. }
  292. // matchError returns true if errors match, false if they don't, compares by error message only for convenience which should be sufficient for these tests
  293. func matchErrors(e1, e2 error) bool {
  294. if e1 == nil && e2 == nil {
  295. return true
  296. }
  297. if e1 != nil && e2 != nil {
  298. return e1.Error() == e2.Error()
  299. }
  300. return false
  301. }
  302. // testServerAndClientConfig returns a server that listens and a config that can reference it
  303. func testServerAndClientConfig(handler func(http.ResponseWriter, *http.Request)) (*httptest.Server, *restclient.Config) {
  304. srv := httptest.NewServer(http.HandlerFunc(handler))
  305. config := &restclient.Config{
  306. Host: srv.URL,
  307. }
  308. return srv, config
  309. }
  310. // fakeAction records information about requests to aid in testing.
  311. type fakeAction struct {
  312. method string
  313. path string
  314. }
  315. // String returns method=path to aid in testing
  316. func (f *fakeAction) String() string {
  317. return strings.Join([]string{f.method, f.path}, "=")
  318. }
  319. // fakeActionHandler holds a list of fakeActions received
  320. type fakeActionHandler struct {
  321. // statusCode returned by this handler
  322. statusCode int
  323. lock sync.Mutex
  324. actions []fakeAction
  325. }
  326. // ServeHTTP logs the action that occurred and always returns the associated status code
  327. func (f *fakeActionHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
  328. f.lock.Lock()
  329. defer f.lock.Unlock()
  330. f.actions = append(f.actions, fakeAction{method: request.Method, path: request.URL.Path})
  331. response.Header().Set("Content-Type", runtime.ContentTypeJSON)
  332. response.WriteHeader(f.statusCode)
  333. response.Write([]byte("{\"apiVersion\": \"v1\", \"kind\": \"List\",\"items\":null}"))
  334. }
  335. // testResources returns a mocked up set of resources across different api groups for testing namespace controller.
  336. func testResources() []*metav1.APIResourceList {
  337. results := []*metav1.APIResourceList{
  338. {
  339. GroupVersion: "v1",
  340. APIResources: []metav1.APIResource{
  341. {
  342. Name: "pods",
  343. Namespaced: true,
  344. Kind: "Pod",
  345. Verbs: []string{"get", "list", "delete", "deletecollection", "create", "update"},
  346. },
  347. {
  348. Name: "services",
  349. Namespaced: true,
  350. Kind: "Service",
  351. Verbs: []string{"get", "list", "delete", "deletecollection", "create", "update"},
  352. },
  353. },
  354. },
  355. {
  356. GroupVersion: "apps/v1",
  357. APIResources: []metav1.APIResource{
  358. {
  359. Name: "deployments",
  360. Namespaced: true,
  361. Kind: "Deployment",
  362. Verbs: []string{"get", "list", "delete", "deletecollection", "create", "update"},
  363. },
  364. },
  365. },
  366. }
  367. return results
  368. }