client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /*
  2. Copyright 2016 The Rook Authors. All rights reserved.
  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 test
  14. import (
  15. "context"
  16. "encoding/base32"
  17. "fmt"
  18. "strings"
  19. "testing"
  20. "github.com/google/uuid"
  21. appsv1 "k8s.io/api/apps/v1"
  22. batchv1 "k8s.io/api/batch/v1"
  23. corev1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/api/errors"
  25. "k8s.io/apimachinery/pkg/api/meta"
  26. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  27. "k8s.io/apimachinery/pkg/runtime"
  28. "k8s.io/apimachinery/pkg/runtime/schema"
  29. "k8s.io/apimachinery/pkg/version"
  30. fakediscovery "k8s.io/client-go/discovery/fake"
  31. "k8s.io/client-go/kubernetes/fake"
  32. k8stesting "k8s.io/client-go/testing"
  33. )
  34. // New creates a fake K8s cluster with some nodes added.
  35. func New(t *testing.T, nodes int) *fake.Clientset {
  36. t.Helper()
  37. clientset := fake.NewSimpleClientset()
  38. AddSomeReadyNodes(t, clientset, nodes)
  39. return clientset
  40. }
  41. // AddReadyNode adds a new Node with status "Ready" and the given name and IP.
  42. func AddReadyNode(t *testing.T, clientset *fake.Clientset, name, ip string) {
  43. t.Helper()
  44. ready := corev1.NodeCondition{Type: corev1.NodeReady, Status: corev1.ConditionTrue}
  45. n := &corev1.Node{
  46. ObjectMeta: metav1.ObjectMeta{
  47. Labels: map[string]string{
  48. corev1.LabelHostname: name,
  49. },
  50. Name: name,
  51. },
  52. Status: corev1.NodeStatus{
  53. Conditions: []corev1.NodeCondition{
  54. ready,
  55. },
  56. Addresses: []corev1.NodeAddress{
  57. {
  58. Type: corev1.NodeInternalIP,
  59. Address: ip,
  60. },
  61. },
  62. },
  63. }
  64. _, err := clientset.CoreV1().Nodes().Create(context.TODO(), n, metav1.CreateOptions{})
  65. if err != nil {
  66. if errors.IsAlreadyExists(err) {
  67. t.Logf("AddReadyNode: node %q already exists; not treating this as an error", n.Name)
  68. }
  69. panic(fmt.Errorf("failed to create node %q: %+v", name, err))
  70. }
  71. }
  72. // AddSomeReadyNodes create a number of new, ready Nodes.
  73. // - name from 0 to count-1
  74. // - ip from 0.0.0.0 to <count-1>.<count-1>.<count-1>.<count-1>
  75. func AddSomeReadyNodes(t *testing.T, clientset *fake.Clientset, count int) {
  76. t.Helper()
  77. for i := 0; i < count; i++ {
  78. name := fmt.Sprintf("node%d", i)
  79. ip := fmt.Sprintf("%d.%d.%d.%d", i, i, i, i)
  80. AddReadyNode(t, clientset, name, ip)
  81. }
  82. }
  83. // SetFakeKubernetesVersion sets a fake K8s version on the clientset. Version must be in semver
  84. // format with a preceding "v" (e.g., "v1.13.2").
  85. func SetFakeKubernetesVersion(clientset *fake.Clientset, semver string) {
  86. d := clientset.Discovery()
  87. fd, ok := d.(*fakediscovery.FakeDiscovery)
  88. if !ok {
  89. panic(fmt.Errorf("failed to get fake clientset's fake discovery"))
  90. }
  91. numOnly := semver[1:] // remove preceding v
  92. xyz := strings.Split(numOnly, ".")
  93. if len(xyz) != 3 {
  94. panic(fmt.Errorf("version not in 'vX.Y.Z' format: %s", semver))
  95. }
  96. fd.FakedServerVersion =
  97. &version.Info{
  98. Major: xyz[0],
  99. Minor: xyz[1],
  100. GitVersion: semver,
  101. }
  102. }
  103. var (
  104. // Change this if we move away from corev1.
  105. podGVR schema.GroupVersionResource = corev1.SchemeGroupVersion.WithResource(corev1.ResourcePods.String())
  106. // Change this if we move away from corev1. Name of the Kind is always the same name as the struct
  107. // which the Go client lib references, capitalized (e.g. "Node" or "Pod").
  108. nodeGVK schema.GroupVersionKind = corev1.SchemeGroupVersion.WithKind("Node")
  109. // Change this if we move away from corev1. corev1 doesn't define ResourceNodes like it does
  110. // ResourcePods so we must use the "nodes" string which is unlikely to change.
  111. nodeGVR schema.GroupVersionResource = corev1.SchemeGroupVersion.WithResource("nodes")
  112. )
  113. // NewComplexClientset is a reusable clientset for Rook unit tests that adds some complex behavior
  114. // to the clientset to mimic more of what K8s does in the real world.
  115. // - Generate a name for resources that have 'generateName' set and 'name' unset.
  116. func NewComplexClientset(t *testing.T) *fake.Clientset {
  117. t.Helper()
  118. clientset := fake.NewSimpleClientset()
  119. // Some resources are created with generateName used, and we need to capture the create
  120. // calls and generate a name for them in order for them to all have unique names and to
  121. // replicate the behavior of k8s in the wild.
  122. var generateNameReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
  123. createAction, ok := action.(k8stesting.CreateAction)
  124. if !ok {
  125. panic(fmt.Errorf("action is not a create action: %+v", action))
  126. }
  127. obj := createAction.GetObject()
  128. objMeta, err := meta.Accessor(obj)
  129. if err != nil {
  130. panic(fmt.Errorf("failed to objMeta"))
  131. }
  132. resource := action.GetResource().Resource
  133. name := objMeta.GetName()
  134. if name == "" {
  135. genName := objMeta.GetGenerateName()
  136. if genName == "" {
  137. panic(fmt.Errorf("object does not have name or generateName set: %+v", obj))
  138. }
  139. // generate a uuid to add a random postfix to generateName
  140. b := [16]byte(uuid.New())
  141. // use base32 encoding to create a shorter uuid
  142. b32 := base32.StdEncoding.EncodeToString(b[:]) // includes trailing equal signs
  143. newName := genName + "-" + strings.Trim(b32, "=") // trim off the trailing equal signs
  144. objMeta.SetName(newName)
  145. t.Logf("generateName reactor: generated name for %s: %s", resource, objMeta.GetName())
  146. }
  147. // setting obj.Name above modifies the action in-place before future reactors occur
  148. // we want the default reactor to create the resource, so return false as if we did nothing
  149. return false, nil, nil
  150. }
  151. clientset.PrependReactor("create", "*", generateNameReactor)
  152. return clientset
  153. }
  154. // PrependComplexJobReactor adds a Job reactor with the below behavior. If more or different
  155. // functionality than this is needed for a test, either make a custom Job reactor or add more
  156. // optional behavior to this reactor.
  157. // - When a Job is created, create the Pod for the Job based on the Job's Pod template
  158. // - Created pod.Name = "[job name]-[pod name in job template]"
  159. // - When a Job is deleted, delete the Pod for the Job (Pod delete will not be handled by reactors)
  160. // - Pod create/delete is done to the clientset tracker, so no Pod watch events will register.
  161. // - Optionally look through the clientset Nodes to randomly assign created Pods to a node.
  162. func PrependComplexJobReactor(t *testing.T, clientset *fake.Clientset, assignPodToNode bool) {
  163. t.Helper()
  164. var jobReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
  165. switch action := action.(type) {
  166. case k8stesting.CreateActionImpl:
  167. obj := action.GetObject()
  168. job, ok := obj.(*batchv1.Job)
  169. if !ok {
  170. panic(fmt.Errorf("object is not a job: %+v", obj))
  171. }
  172. pod := corev1.Pod{
  173. ObjectMeta: *job.Spec.Template.ObjectMeta.DeepCopy(),
  174. Spec: *job.Spec.Template.Spec.DeepCopy(),
  175. }
  176. pod.SetName(job.GetName() + "-" + pod.GetName())
  177. if assignPodToNode {
  178. pod.Spec.NodeName = pickNode(clientset)
  179. }
  180. // cannot use clientset.CoreV1().Pods(ns).Create() b/c the fake clientset locks the
  181. // object tracker during reactors.
  182. err := clientset.Tracker().Create(podGVR, &pod, action.GetNamespace())
  183. if err != nil {
  184. if errors.IsAlreadyExists(err) {
  185. t.Logf("job reactor: pod %q is already created for job %q; not treating this as an error", pod.GetName(), job.GetName())
  186. }
  187. panic(fmt.Errorf("failed to create Pod %q for job %+v. %v", pod.Name, job, err))
  188. }
  189. t.Logf("job reactor: created pod %q for job %q", pod.Name, job.Name)
  190. case k8stesting.DeleteActionImpl:
  191. jobName := action.GetName()
  192. obj, err := clientset.Tracker().Get(action.GetResource(), action.GetNamespace(), jobName)
  193. if err != nil && !errors.IsNotFound(err) {
  194. if errors.IsNotFound(err) {
  195. t.Logf("job reactor: job %q being deleted does not exist; will not delete a pod", jobName)
  196. return false, nil, nil
  197. }
  198. panic(fmt.Errorf("failed to get info about job %q being deleted. %+v", jobName, err))
  199. }
  200. job, ok := obj.(*batchv1.Job)
  201. if !ok {
  202. panic(fmt.Errorf("object not a job: %+v", obj))
  203. }
  204. podName := job.GetName() + "-" + job.Spec.Template.GetName()
  205. err = clientset.Tracker().Delete(podGVR, action.GetNamespace(), podName)
  206. if err != nil {
  207. if errors.IsNotFound(err) {
  208. t.Logf("job reactor: pod %q does not exist to be deleted while deleting job %q", podName, jobName)
  209. return false, nil, nil
  210. }
  211. panic(fmt.Errorf("failed to delete pod %q while deleting job %+v", podName, job))
  212. }
  213. t.Logf("job reactor: deleted pod %q while deleting job %q", podName, jobName)
  214. }
  215. return false, nil, nil
  216. }
  217. clientset.PrependReactor("*", "jobs", jobReactor)
  218. }
  219. // PrependFailReactor adds a reactor with the desired verb and resource that will report a failure.
  220. func PrependFailReactor(t *testing.T, clientset *fake.Clientset, verb, resource string) {
  221. t.Helper()
  222. var failReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
  223. return true, nil, fmt.Errorf("fail reactor: induced failure")
  224. }
  225. clientset.PrependReactor(verb, resource, failReactor)
  226. }
  227. var pickNodeIdx = 0 // global, used to allow round-robin node picking
  228. // Pick a node name from the nodes present in the fake K8s clientset cluster.
  229. func pickNode(clientset *fake.Clientset) string {
  230. obj, err := clientset.Tracker().List(nodeGVR, nodeGVK, corev1.NamespaceAll)
  231. if err != nil {
  232. panic(fmt.Errorf("pickNode: failed to list nodes. %+v", err))
  233. }
  234. nodes, ok := obj.(*corev1.NodeList)
  235. if !ok {
  236. panic(fmt.Errorf("pickNode: tracker did not return valid NodeList: %+v", obj))
  237. }
  238. if len(nodes.Items) == 0 {
  239. panic(fmt.Errorf("pickNode: no nodes are available in the fake clientset to pick from"))
  240. }
  241. pickNodeIdx = pickNodeIdx % len(nodes.Items) // reset to 0 once idx is more than number of nodes
  242. name := nodes.Items[pickNodeIdx].GetName()
  243. pickNodeIdx++
  244. return name
  245. }
  246. func FakeOperatorPod(ns string) *corev1.Pod {
  247. p := &corev1.Pod{
  248. ObjectMeta: metav1.ObjectMeta{
  249. Name: "rook-ceph-operator",
  250. Namespace: ns,
  251. OwnerReferences: []metav1.OwnerReference{
  252. {
  253. Kind: "ReplicaSet",
  254. Name: "testReplicaSet",
  255. },
  256. },
  257. },
  258. Spec: corev1.PodSpec{},
  259. }
  260. return p
  261. }
  262. func FakeReplicaSet(ns string) *appsv1.ReplicaSet {
  263. r := &appsv1.ReplicaSet{
  264. ObjectMeta: metav1.ObjectMeta{
  265. Name: "testReplicaSet",
  266. Namespace: ns,
  267. OwnerReferences: []metav1.OwnerReference{
  268. {
  269. Kind: "Deployment",
  270. },
  271. },
  272. },
  273. }
  274. return r
  275. }
  276. func FakeCustomisePodCreate(t *testing.T, clientset *fake.Clientset, name, ns string, label map[string]string) {
  277. pod := &corev1.Pod{
  278. ObjectMeta: metav1.ObjectMeta{
  279. Name: name,
  280. Namespace: ns,
  281. OwnerReferences: []metav1.OwnerReference{
  282. {
  283. Kind: "Deployment",
  284. },
  285. },
  286. Labels: label,
  287. },
  288. }
  289. err := clientset.Tracker().Create(podGVR, pod, ns)
  290. if err != nil {
  291. if errors.IsAlreadyExists(err) {
  292. t.Logf("pod %q is already created", pod.GetName())
  293. }
  294. panic(fmt.Errorf("failed to create Pod %q. %v", pod.Name, err))
  295. }
  296. t.Logf("job reactor: created pod %q ", pod.Name)
  297. }