apply_test.go 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415
  1. /*
  2. Copyright 2014 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 apply
  14. import (
  15. "bytes"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "io"
  20. "io/ioutil"
  21. "net/http"
  22. "os"
  23. "path/filepath"
  24. "strings"
  25. "testing"
  26. "github.com/googleapis/gnostic/OpenAPIv2"
  27. "github.com/spf13/cobra"
  28. appsv1 "k8s.io/api/apps/v1"
  29. corev1 "k8s.io/api/core/v1"
  30. kubeerr "k8s.io/apimachinery/pkg/api/errors"
  31. "k8s.io/apimachinery/pkg/api/meta"
  32. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  33. "k8s.io/apimachinery/pkg/runtime"
  34. "k8s.io/apimachinery/pkg/runtime/schema"
  35. sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
  36. "k8s.io/cli-runtime/pkg/genericclioptions"
  37. "k8s.io/cli-runtime/pkg/resource"
  38. dynamicfakeclient "k8s.io/client-go/dynamic/fake"
  39. restclient "k8s.io/client-go/rest"
  40. "k8s.io/client-go/rest/fake"
  41. clienttesting "k8s.io/client-go/testing"
  42. cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
  43. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  44. "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
  45. "k8s.io/kubernetes/pkg/kubectl/scheme"
  46. utilpointer "k8s.io/utils/pointer"
  47. )
  48. var (
  49. fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "..", "api", "openapi-spec", "swagger.json")}
  50. testingOpenAPISchemaFns = []func() (openapi.Resources, error){nil, AlwaysErrorOpenAPISchemaFn, openAPISchemaFn}
  51. AlwaysErrorOpenAPISchemaFn = func() (openapi.Resources, error) {
  52. return nil, errors.New("cannot get openapi spec")
  53. }
  54. openAPISchemaFn = func() (openapi.Resources, error) {
  55. s, err := fakeSchema.OpenAPISchema()
  56. if err != nil {
  57. return nil, err
  58. }
  59. return openapi.NewOpenAPIData(s)
  60. }
  61. codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
  62. )
  63. func TestApplyExtraArgsFail(t *testing.T) {
  64. f := cmdtesting.NewTestFactory()
  65. defer f.Cleanup()
  66. c := NewCmdApply("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
  67. if validateApplyArgs(c, []string{"rc"}) == nil {
  68. t.Fatalf("unexpected non-error")
  69. }
  70. }
  71. func validateApplyArgs(cmd *cobra.Command, args []string) error {
  72. if len(args) != 0 {
  73. return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
  74. }
  75. return nil
  76. }
  77. const (
  78. filenameCM = "../../../../test/fixtures/pkg/kubectl/cmd/apply/cm.yaml"
  79. filenameRC = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc.yaml"
  80. filenameRCArgs = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc-args.yaml"
  81. filenameRCLastAppliedArgs = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied-args.yaml"
  82. filenameRCNoAnnotation = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc-no-annotation.yaml"
  83. filenameRCLASTAPPLIED = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc-lastapplied.yaml"
  84. filenameSVC = "../../../../test/fixtures/pkg/kubectl/cmd/apply/service.yaml"
  85. filenameRCSVC = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc-service.yaml"
  86. filenameNoExistRC = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc-noexist.yaml"
  87. filenameRCPatchTest = "../../../../test/fixtures/pkg/kubectl/cmd/apply/patch.json"
  88. dirName = "../../../../test/fixtures/pkg/kubectl/cmd/apply/testdir"
  89. filenameRCJSON = "../../../../test/fixtures/pkg/kubectl/cmd/apply/rc.json"
  90. filenameWidgetClientside = "../../../../test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml"
  91. filenameWidgetServerside = "../../../../test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml"
  92. filenameDeployObjServerside = "../../../../test/fixtures/pkg/kubectl/cmd/apply/deploy-serverside.yaml"
  93. filenameDeployObjClientside = "../../../../test/fixtures/pkg/kubectl/cmd/apply/deploy-clientside.yaml"
  94. )
  95. func readConfigMapList(t *testing.T, filename string) [][]byte {
  96. data := readBytesFromFile(t, filename)
  97. cmList := corev1.ConfigMapList{}
  98. if err := runtime.DecodeInto(codec, data, &cmList); err != nil {
  99. t.Fatal(err)
  100. }
  101. var listCmBytes [][]byte
  102. for _, cm := range cmList.Items {
  103. cmBytes, err := runtime.Encode(codec, &cm)
  104. if err != nil {
  105. t.Fatal(err)
  106. }
  107. listCmBytes = append(listCmBytes, cmBytes)
  108. }
  109. return listCmBytes
  110. }
  111. func readBytesFromFile(t *testing.T, filename string) []byte {
  112. file, err := os.Open(filename)
  113. if err != nil {
  114. t.Fatal(err)
  115. }
  116. defer file.Close()
  117. data, err := ioutil.ReadAll(file)
  118. if err != nil {
  119. t.Fatal(err)
  120. }
  121. return data
  122. }
  123. func readReplicationController(t *testing.T, filenameRC string) (string, []byte) {
  124. rcObj := readReplicationControllerFromFile(t, filenameRC)
  125. metaAccessor, err := meta.Accessor(rcObj)
  126. if err != nil {
  127. t.Fatal(err)
  128. }
  129. rcBytes, err := runtime.Encode(codec, rcObj)
  130. if err != nil {
  131. t.Fatal(err)
  132. }
  133. return metaAccessor.GetName(), rcBytes
  134. }
  135. func readReplicationControllerFromFile(t *testing.T, filename string) *corev1.ReplicationController {
  136. data := readBytesFromFile(t, filename)
  137. rc := corev1.ReplicationController{}
  138. if err := runtime.DecodeInto(codec, data, &rc); err != nil {
  139. t.Fatal(err)
  140. }
  141. return &rc
  142. }
  143. func readUnstructuredFromFile(t *testing.T, filename string) *unstructured.Unstructured {
  144. data := readBytesFromFile(t, filename)
  145. unst := unstructured.Unstructured{}
  146. if err := runtime.DecodeInto(codec, data, &unst); err != nil {
  147. t.Fatal(err)
  148. }
  149. return &unst
  150. }
  151. func readServiceFromFile(t *testing.T, filename string) *corev1.Service {
  152. data := readBytesFromFile(t, filename)
  153. svc := corev1.Service{}
  154. if err := runtime.DecodeInto(codec, data, &svc); err != nil {
  155. t.Fatal(err)
  156. }
  157. return &svc
  158. }
  159. func annotateRuntimeObject(t *testing.T, originalObj, currentObj runtime.Object, kind string) (string, []byte) {
  160. originalAccessor, err := meta.Accessor(originalObj)
  161. if err != nil {
  162. t.Fatal(err)
  163. }
  164. // The return value of this function is used in the body of the GET
  165. // request in the unit tests. Here we are adding a misc label to the object.
  166. // In tests, the validatePatchApplication() gets called in PATCH request
  167. // handler in fake round tripper. validatePatchApplication call
  168. // checks that this DELETE_ME label was deleted by the apply implementation in
  169. // kubectl.
  170. originalLabels := originalAccessor.GetLabels()
  171. originalLabels["DELETE_ME"] = "DELETE_ME"
  172. originalAccessor.SetLabels(originalLabels)
  173. original, err := runtime.Encode(unstructured.JSONFallbackEncoder{Encoder: codec}, originalObj)
  174. if err != nil {
  175. t.Fatal(err)
  176. }
  177. currentAccessor, err := meta.Accessor(currentObj)
  178. if err != nil {
  179. t.Fatal(err)
  180. }
  181. currentAnnotations := currentAccessor.GetAnnotations()
  182. if currentAnnotations == nil {
  183. currentAnnotations = make(map[string]string)
  184. }
  185. currentAnnotations[corev1.LastAppliedConfigAnnotation] = string(original)
  186. currentAccessor.SetAnnotations(currentAnnotations)
  187. current, err := runtime.Encode(unstructured.JSONFallbackEncoder{Encoder: codec}, currentObj)
  188. if err != nil {
  189. t.Fatal(err)
  190. }
  191. return currentAccessor.GetName(), current
  192. }
  193. func readAndAnnotateReplicationController(t *testing.T, filename string) (string, []byte) {
  194. rc1 := readReplicationControllerFromFile(t, filename)
  195. rc2 := readReplicationControllerFromFile(t, filename)
  196. return annotateRuntimeObject(t, rc1, rc2, "ReplicationController")
  197. }
  198. func readAndAnnotateService(t *testing.T, filename string) (string, []byte) {
  199. svc1 := readServiceFromFile(t, filename)
  200. svc2 := readServiceFromFile(t, filename)
  201. return annotateRuntimeObject(t, svc1, svc2, "Service")
  202. }
  203. func readAndAnnotateUnstructured(t *testing.T, filename string) (string, []byte) {
  204. obj1 := readUnstructuredFromFile(t, filename)
  205. obj2 := readUnstructuredFromFile(t, filename)
  206. return annotateRuntimeObject(t, obj1, obj2, "Widget")
  207. }
  208. func validatePatchApplication(t *testing.T, req *http.Request) {
  209. patch, err := ioutil.ReadAll(req.Body)
  210. if err != nil {
  211. t.Fatal(err)
  212. }
  213. patchMap := map[string]interface{}{}
  214. if err := json.Unmarshal(patch, &patchMap); err != nil {
  215. t.Fatal(err)
  216. }
  217. annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
  218. if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok {
  219. t.Fatalf("patch does not contain annotation:\n%s\n", patch)
  220. }
  221. labelMap := walkMapPath(t, patchMap, []string{"metadata", "labels"})
  222. if deleteMe, ok := labelMap["DELETE_ME"]; !ok || deleteMe != nil {
  223. t.Fatalf("patch does not remove deleted key: DELETE_ME:\n%s\n", patch)
  224. }
  225. }
  226. func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[string]interface{} {
  227. finish := start
  228. for i := 0; i < len(path); i++ {
  229. var ok bool
  230. finish, ok = finish[path[i]].(map[string]interface{})
  231. if !ok {
  232. t.Fatalf("key:%s of path:%v not found in map:%v", path[i], path, start)
  233. }
  234. }
  235. return finish
  236. }
  237. func TestRunApplyPrintsValidObjectList(t *testing.T) {
  238. cmdtesting.InitTestErrorHandler(t)
  239. configMapList := readConfigMapList(t, filenameCM)
  240. pathCM := "/namespaces/test/configmaps"
  241. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  242. defer tf.Cleanup()
  243. tf.UnstructuredClient = &fake.RESTClient{
  244. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  245. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  246. switch p, m := req.URL.Path, req.Method; {
  247. case strings.HasPrefix(p, pathCM) && m == "GET":
  248. fallthrough
  249. case strings.HasPrefix(p, pathCM) && m == "PATCH":
  250. var body io.ReadCloser
  251. switch p {
  252. case pathCM + "/test0":
  253. body = ioutil.NopCloser(bytes.NewReader(configMapList[0]))
  254. case pathCM + "/test1":
  255. body = ioutil.NopCloser(bytes.NewReader(configMapList[1]))
  256. default:
  257. t.Errorf("unexpected request to %s", p)
  258. }
  259. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  260. default:
  261. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  262. return nil, nil
  263. }
  264. }),
  265. }
  266. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  267. ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
  268. cmd := NewCmdApply("kubectl", tf, ioStreams)
  269. cmd.Flags().Set("filename", filenameCM)
  270. cmd.Flags().Set("output", "json")
  271. cmd.Flags().Set("dry-run", "true")
  272. cmd.Run(cmd, []string{})
  273. // ensure that returned list can be unmarshaled back into a configmap list
  274. cmList := corev1.List{}
  275. if err := runtime.DecodeInto(codec, buf.Bytes(), &cmList); err != nil {
  276. t.Fatal(err)
  277. }
  278. if len(cmList.Items) != 2 {
  279. t.Fatalf("Expected 2 items in the result; got %d", len(cmList.Items))
  280. }
  281. if !strings.Contains(string(cmList.Items[0].Raw), "key1") {
  282. t.Fatalf("Did not get first ConfigMap at the first position")
  283. }
  284. if !strings.Contains(string(cmList.Items[1].Raw), "key2") {
  285. t.Fatalf("Did not get second ConfigMap at the second position")
  286. }
  287. }
  288. func TestRunApplyViewLastApplied(t *testing.T) {
  289. _, rcBytesWithConfig := readReplicationController(t, filenameRCLASTAPPLIED)
  290. _, rcBytesWithArgs := readReplicationController(t, filenameRCLastAppliedArgs)
  291. nameRC, rcBytes := readReplicationController(t, filenameRC)
  292. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  293. tests := []struct {
  294. name, nameRC, pathRC, filePath, outputFormat, expectedErr, expectedOut, selector string
  295. args []string
  296. respBytes []byte
  297. }{
  298. {
  299. name: "view with file",
  300. filePath: filenameRC,
  301. outputFormat: "",
  302. expectedErr: "",
  303. expectedOut: "test: 1234\n",
  304. selector: "",
  305. args: []string{},
  306. respBytes: rcBytesWithConfig,
  307. },
  308. {
  309. name: "test with file include `%s` in arguments",
  310. filePath: filenameRCArgs,
  311. outputFormat: "",
  312. expectedErr: "",
  313. expectedOut: "args: -random_flag=%s@domain.com\n",
  314. selector: "",
  315. args: []string{},
  316. respBytes: rcBytesWithArgs,
  317. },
  318. {
  319. name: "view with file json format",
  320. filePath: filenameRC,
  321. outputFormat: "json",
  322. expectedErr: "",
  323. expectedOut: "{\n \"test\": 1234\n}\n",
  324. selector: "",
  325. args: []string{},
  326. respBytes: rcBytesWithConfig,
  327. },
  328. {
  329. name: "view resource/name invalid format",
  330. filePath: "",
  331. outputFormat: "wide",
  332. expectedErr: "error: Unexpected -o output mode: wide, the flag 'output' must be one of yaml|json\nSee 'view-last-applied -h' for help and examples",
  333. expectedOut: "",
  334. selector: "",
  335. args: []string{"replicationcontroller", "test-rc"},
  336. respBytes: rcBytesWithConfig,
  337. },
  338. {
  339. name: "view resource with label",
  340. filePath: "",
  341. outputFormat: "",
  342. expectedErr: "",
  343. expectedOut: "test: 1234\n",
  344. selector: "name=test-rc",
  345. args: []string{"replicationcontroller"},
  346. respBytes: rcBytesWithConfig,
  347. },
  348. {
  349. name: "view resource without annotations",
  350. filePath: "",
  351. outputFormat: "",
  352. expectedErr: "error: no last-applied-configuration annotation found on resource: test-rc",
  353. expectedOut: "",
  354. selector: "",
  355. args: []string{"replicationcontroller", "test-rc"},
  356. respBytes: rcBytes,
  357. },
  358. {
  359. name: "view resource no match",
  360. filePath: "",
  361. outputFormat: "",
  362. expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-match)",
  363. expectedOut: "",
  364. selector: "",
  365. args: []string{"replicationcontroller", "no-match"},
  366. respBytes: nil,
  367. },
  368. }
  369. for _, test := range tests {
  370. t.Run(test.name, func(t *testing.T) {
  371. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  372. defer tf.Cleanup()
  373. tf.UnstructuredClient = &fake.RESTClient{
  374. GroupVersion: schema.GroupVersion{Version: "v1"},
  375. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  376. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  377. switch p, m := req.URL.Path, req.Method; {
  378. case p == pathRC && m == "GET":
  379. bodyRC := ioutil.NopCloser(bytes.NewReader(test.respBytes))
  380. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  381. case p == "/namespaces/test/replicationcontrollers" && m == "GET":
  382. bodyRC := ioutil.NopCloser(bytes.NewReader(test.respBytes))
  383. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  384. case p == "/namespaces/test/replicationcontrollers/no-match" && m == "GET":
  385. return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil
  386. case p == "/api/v1/namespaces/test" && m == "GET":
  387. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil
  388. default:
  389. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  390. return nil, nil
  391. }
  392. }),
  393. }
  394. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  395. cmdutil.BehaviorOnFatal(func(str string, code int) {
  396. if str != test.expectedErr {
  397. t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr)
  398. }
  399. })
  400. ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
  401. cmd := NewCmdApplyViewLastApplied(tf, ioStreams)
  402. if test.filePath != "" {
  403. cmd.Flags().Set("filename", test.filePath)
  404. }
  405. if test.outputFormat != "" {
  406. cmd.Flags().Set("output", test.outputFormat)
  407. }
  408. if test.selector != "" {
  409. cmd.Flags().Set("selector", test.selector)
  410. }
  411. cmd.Run(cmd, test.args)
  412. if buf.String() != test.expectedOut {
  413. t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut)
  414. }
  415. })
  416. }
  417. }
  418. func TestApplyObjectWithoutAnnotation(t *testing.T) {
  419. cmdtesting.InitTestErrorHandler(t)
  420. nameRC, rcBytes := readReplicationController(t, filenameRC)
  421. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  422. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  423. defer tf.Cleanup()
  424. tf.UnstructuredClient = &fake.RESTClient{
  425. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  426. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  427. switch p, m := req.URL.Path, req.Method; {
  428. case p == pathRC && m == "GET":
  429. bodyRC := ioutil.NopCloser(bytes.NewReader(rcBytes))
  430. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  431. case p == pathRC && m == "PATCH":
  432. bodyRC := ioutil.NopCloser(bytes.NewReader(rcBytes))
  433. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  434. default:
  435. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  436. return nil, nil
  437. }
  438. }),
  439. }
  440. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  441. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  442. cmd := NewCmdApply("kubectl", tf, ioStreams)
  443. cmd.Flags().Set("filename", filenameRC)
  444. cmd.Flags().Set("output", "name")
  445. cmd.Run(cmd, []string{})
  446. // uses the name from the file, not the response
  447. expectRC := "replicationcontroller/" + nameRC + "\n"
  448. expectWarning := fmt.Sprintf(warningNoLastAppliedConfigAnnotation, "kubectl")
  449. if errBuf.String() != expectWarning {
  450. t.Fatalf("unexpected non-warning: %s\nexpected: %s", errBuf.String(), expectWarning)
  451. }
  452. if buf.String() != expectRC {
  453. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
  454. }
  455. }
  456. func TestApplyObject(t *testing.T) {
  457. cmdtesting.InitTestErrorHandler(t)
  458. nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
  459. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  460. for _, fn := range testingOpenAPISchemaFns {
  461. t.Run("test apply when a local object is specified", func(t *testing.T) {
  462. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  463. defer tf.Cleanup()
  464. tf.UnstructuredClient = &fake.RESTClient{
  465. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  466. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  467. switch p, m := req.URL.Path, req.Method; {
  468. case p == pathRC && m == "GET":
  469. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  470. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  471. case p == pathRC && m == "PATCH":
  472. validatePatchApplication(t, req)
  473. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  474. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  475. default:
  476. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  477. return nil, nil
  478. }
  479. }),
  480. }
  481. tf.OpenAPISchemaFunc = fn
  482. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  483. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  484. cmd := NewCmdApply("kubectl", tf, ioStreams)
  485. cmd.Flags().Set("filename", filenameRC)
  486. cmd.Flags().Set("output", "name")
  487. cmd.Run(cmd, []string{})
  488. // uses the name from the file, not the response
  489. expectRC := "replicationcontroller/" + nameRC + "\n"
  490. if buf.String() != expectRC {
  491. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
  492. }
  493. if errBuf.String() != "" {
  494. t.Fatalf("unexpected error output: %s", errBuf.String())
  495. }
  496. })
  497. }
  498. }
  499. func TestApplyObjectOutput(t *testing.T) {
  500. cmdtesting.InitTestErrorHandler(t)
  501. nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
  502. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  503. // Add some extra data to the post-patch object
  504. postPatchObj := &unstructured.Unstructured{}
  505. if err := json.Unmarshal(currentRC, &postPatchObj.Object); err != nil {
  506. t.Fatal(err)
  507. }
  508. postPatchLabels := postPatchObj.GetLabels()
  509. if postPatchLabels == nil {
  510. postPatchLabels = map[string]string{}
  511. }
  512. postPatchLabels["post-patch"] = "value"
  513. postPatchObj.SetLabels(postPatchLabels)
  514. postPatchData, err := json.Marshal(postPatchObj)
  515. if err != nil {
  516. t.Fatal(err)
  517. }
  518. for _, fn := range testingOpenAPISchemaFns {
  519. t.Run("test apply returns correct output", func(t *testing.T) {
  520. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  521. defer tf.Cleanup()
  522. tf.UnstructuredClient = &fake.RESTClient{
  523. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  524. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  525. switch p, m := req.URL.Path, req.Method; {
  526. case p == pathRC && m == "GET":
  527. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  528. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  529. case p == pathRC && m == "PATCH":
  530. validatePatchApplication(t, req)
  531. bodyRC := ioutil.NopCloser(bytes.NewReader(postPatchData))
  532. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  533. default:
  534. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  535. return nil, nil
  536. }
  537. }),
  538. }
  539. tf.OpenAPISchemaFunc = fn
  540. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  541. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  542. cmd := NewCmdApply("kubectl", tf, ioStreams)
  543. cmd.Flags().Set("filename", filenameRC)
  544. cmd.Flags().Set("output", "yaml")
  545. cmd.Run(cmd, []string{})
  546. if !strings.Contains(buf.String(), "test-rc") {
  547. t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "test-rc")
  548. }
  549. if !strings.Contains(buf.String(), "post-patch: value") {
  550. t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "post-patch: value")
  551. }
  552. if errBuf.String() != "" {
  553. t.Fatalf("unexpected error output: %s", errBuf.String())
  554. }
  555. })
  556. }
  557. }
  558. func TestApplyRetry(t *testing.T) {
  559. cmdtesting.InitTestErrorHandler(t)
  560. nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
  561. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  562. for _, fn := range testingOpenAPISchemaFns {
  563. t.Run("test apply retries on conflict error", func(t *testing.T) {
  564. firstPatch := true
  565. retry := false
  566. getCount := 0
  567. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  568. defer tf.Cleanup()
  569. tf.UnstructuredClient = &fake.RESTClient{
  570. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  571. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  572. switch p, m := req.URL.Path, req.Method; {
  573. case p == pathRC && m == "GET":
  574. getCount++
  575. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  576. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  577. case p == pathRC && m == "PATCH":
  578. if firstPatch {
  579. firstPatch = false
  580. statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first"))
  581. bodyBytes, _ := json.Marshal(statusErr)
  582. bodyErr := ioutil.NopCloser(bytes.NewReader(bodyBytes))
  583. return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil
  584. }
  585. retry = true
  586. validatePatchApplication(t, req)
  587. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  588. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  589. default:
  590. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  591. return nil, nil
  592. }
  593. }),
  594. }
  595. tf.OpenAPISchemaFunc = fn
  596. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  597. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  598. cmd := NewCmdApply("kubectl", tf, ioStreams)
  599. cmd.Flags().Set("filename", filenameRC)
  600. cmd.Flags().Set("output", "name")
  601. cmd.Run(cmd, []string{})
  602. if !retry || getCount != 2 {
  603. t.Fatalf("apply didn't retry when get conflict error")
  604. }
  605. // uses the name from the file, not the response
  606. expectRC := "replicationcontroller/" + nameRC + "\n"
  607. if buf.String() != expectRC {
  608. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
  609. }
  610. if errBuf.String() != "" {
  611. t.Fatalf("unexpected error output: %s", errBuf.String())
  612. }
  613. })
  614. }
  615. }
  616. func TestApplyNonExistObject(t *testing.T) {
  617. nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
  618. pathRC := "/namespaces/test/replicationcontrollers"
  619. pathNameRC := pathRC + "/" + nameRC
  620. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  621. defer tf.Cleanup()
  622. tf.UnstructuredClient = &fake.RESTClient{
  623. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  624. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  625. switch p, m := req.URL.Path, req.Method; {
  626. case p == "/api/v1/namespaces/test" && m == "GET":
  627. return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader(nil))}, nil
  628. case p == pathNameRC && m == "GET":
  629. return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader(nil))}, nil
  630. case p == pathRC && m == "POST":
  631. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  632. return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  633. default:
  634. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  635. return nil, nil
  636. }
  637. }),
  638. }
  639. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  640. ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
  641. cmd := NewCmdApply("kubectl", tf, ioStreams)
  642. cmd.Flags().Set("filename", filenameRC)
  643. cmd.Flags().Set("output", "name")
  644. cmd.Run(cmd, []string{})
  645. // uses the name from the file, not the response
  646. expectRC := "replicationcontroller/" + nameRC + "\n"
  647. if buf.String() != expectRC {
  648. t.Errorf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
  649. }
  650. }
  651. func TestApplyEmptyPatch(t *testing.T) {
  652. cmdtesting.InitTestErrorHandler(t)
  653. nameRC, _ := readAndAnnotateReplicationController(t, filenameRC)
  654. pathRC := "/namespaces/test/replicationcontrollers"
  655. pathNameRC := pathRC + "/" + nameRC
  656. verifyPost := false
  657. var body []byte
  658. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  659. defer tf.Cleanup()
  660. tf.UnstructuredClient = &fake.RESTClient{
  661. GroupVersion: schema.GroupVersion{Version: "v1"},
  662. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  663. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  664. switch p, m := req.URL.Path, req.Method; {
  665. case p == "/api/v1/namespaces/test" && m == "GET":
  666. return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader(nil))}, nil
  667. case p == pathNameRC && m == "GET":
  668. if body == nil {
  669. return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader(nil))}, nil
  670. }
  671. bodyRC := ioutil.NopCloser(bytes.NewReader(body))
  672. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  673. case p == pathRC && m == "POST":
  674. body, _ = ioutil.ReadAll(req.Body)
  675. verifyPost = true
  676. bodyRC := ioutil.NopCloser(bytes.NewReader(body))
  677. return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  678. default:
  679. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  680. return nil, nil
  681. }
  682. }),
  683. }
  684. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  685. // 1. apply non exist object
  686. ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
  687. cmd := NewCmdApply("kubectl", tf, ioStreams)
  688. cmd.Flags().Set("filename", filenameRC)
  689. cmd.Flags().Set("output", "name")
  690. cmd.Run(cmd, []string{})
  691. expectRC := "replicationcontroller/" + nameRC + "\n"
  692. if buf.String() != expectRC {
  693. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
  694. }
  695. if !verifyPost {
  696. t.Fatal("No server-side post call detected")
  697. }
  698. // 2. test apply already exist object, will not send empty patch request
  699. ioStreams, _, buf, _ = genericclioptions.NewTestIOStreams()
  700. cmd = NewCmdApply("kubectl", tf, ioStreams)
  701. cmd.Flags().Set("filename", filenameRC)
  702. cmd.Flags().Set("output", "name")
  703. cmd.Run(cmd, []string{})
  704. if buf.String() != expectRC {
  705. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
  706. }
  707. }
  708. func TestApplyMultipleObjectsAsList(t *testing.T) {
  709. testApplyMultipleObjects(t, true)
  710. }
  711. func TestApplyMultipleObjectsAsFiles(t *testing.T) {
  712. testApplyMultipleObjects(t, false)
  713. }
  714. func testApplyMultipleObjects(t *testing.T, asList bool) {
  715. nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
  716. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  717. nameSVC, currentSVC := readAndAnnotateService(t, filenameSVC)
  718. pathSVC := "/namespaces/test/services/" + nameSVC
  719. for _, fn := range testingOpenAPISchemaFns {
  720. t.Run("test apply on multiple objects", func(t *testing.T) {
  721. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  722. defer tf.Cleanup()
  723. tf.UnstructuredClient = &fake.RESTClient{
  724. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  725. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  726. switch p, m := req.URL.Path, req.Method; {
  727. case p == pathRC && m == "GET":
  728. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  729. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  730. case p == pathRC && m == "PATCH":
  731. validatePatchApplication(t, req)
  732. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  733. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  734. case p == pathSVC && m == "GET":
  735. bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
  736. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil
  737. case p == pathSVC && m == "PATCH":
  738. validatePatchApplication(t, req)
  739. bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
  740. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil
  741. default:
  742. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  743. return nil, nil
  744. }
  745. }),
  746. }
  747. tf.OpenAPISchemaFunc = fn
  748. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  749. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  750. cmd := NewCmdApply("kubectl", tf, ioStreams)
  751. if asList {
  752. cmd.Flags().Set("filename", filenameRCSVC)
  753. } else {
  754. cmd.Flags().Set("filename", filenameRC)
  755. cmd.Flags().Set("filename", filenameSVC)
  756. }
  757. cmd.Flags().Set("output", "name")
  758. cmd.Run(cmd, []string{})
  759. // Names should come from the REST response, NOT the files
  760. expectRC := "replicationcontroller/" + nameRC + "\n"
  761. expectSVC := "service/" + nameSVC + "\n"
  762. // Test both possible orders since output is non-deterministic.
  763. expectOne := expectRC + expectSVC
  764. expectTwo := expectSVC + expectRC
  765. if buf.String() != expectOne && buf.String() != expectTwo {
  766. t.Fatalf("unexpected output: %s\nexpected: %s OR %s", buf.String(), expectOne, expectTwo)
  767. }
  768. if errBuf.String() != "" {
  769. t.Fatalf("unexpected error output: %s", errBuf.String())
  770. }
  771. })
  772. }
  773. }
  774. func readDeploymentFromFile(t *testing.T, file string) []byte {
  775. raw := readBytesFromFile(t, file)
  776. obj := &appsv1.Deployment{}
  777. if err := runtime.DecodeInto(codec, raw, obj); err != nil {
  778. t.Fatal(err)
  779. }
  780. objJSON, err := runtime.Encode(codec, obj)
  781. if err != nil {
  782. t.Fatal(err)
  783. }
  784. return objJSON
  785. }
  786. func TestApplyNULLPreservation(t *testing.T) {
  787. cmdtesting.InitTestErrorHandler(t)
  788. deploymentName := "nginx-deployment"
  789. deploymentPath := "/namespaces/test/deployments/" + deploymentName
  790. verifiedPatch := false
  791. deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside)
  792. for _, fn := range testingOpenAPISchemaFns {
  793. t.Run("test apply preserves NULL fields", func(t *testing.T) {
  794. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  795. defer tf.Cleanup()
  796. tf.UnstructuredClient = &fake.RESTClient{
  797. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  798. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  799. switch p, m := req.URL.Path, req.Method; {
  800. case p == deploymentPath && m == "GET":
  801. body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
  802. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  803. case p == deploymentPath && m == "PATCH":
  804. patch, err := ioutil.ReadAll(req.Body)
  805. if err != nil {
  806. t.Fatal(err)
  807. }
  808. patchMap := map[string]interface{}{}
  809. if err := json.Unmarshal(patch, &patchMap); err != nil {
  810. t.Fatal(err)
  811. }
  812. annotationMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
  813. if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok {
  814. t.Fatalf("patch does not contain annotation:\n%s\n", patch)
  815. }
  816. strategy := walkMapPath(t, patchMap, []string{"spec", "strategy"})
  817. if value, ok := strategy["rollingUpdate"]; !ok || value != nil {
  818. t.Fatalf("patch did not retain null value in key: rollingUpdate:\n%s\n", patch)
  819. }
  820. verifiedPatch = true
  821. // The real API server would had returned the patched object but Kubectl
  822. // is ignoring the actual return object.
  823. // TODO: Make this match actual server behavior by returning the patched object.
  824. body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
  825. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  826. default:
  827. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  828. return nil, nil
  829. }
  830. }),
  831. }
  832. tf.OpenAPISchemaFunc = fn
  833. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  834. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  835. cmd := NewCmdApply("kubectl", tf, ioStreams)
  836. cmd.Flags().Set("filename", filenameDeployObjClientside)
  837. cmd.Flags().Set("output", "name")
  838. cmd.Run(cmd, []string{})
  839. expected := "deployment.apps/" + deploymentName + "\n"
  840. if buf.String() != expected {
  841. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
  842. }
  843. if errBuf.String() != "" {
  844. t.Fatalf("unexpected error output: %s", errBuf.String())
  845. }
  846. if !verifiedPatch {
  847. t.Fatal("No server-side patch call detected")
  848. }
  849. })
  850. }
  851. }
  852. // TestUnstructuredApply checks apply operations on an unstructured object
  853. func TestUnstructuredApply(t *testing.T) {
  854. cmdtesting.InitTestErrorHandler(t)
  855. name, curr := readAndAnnotateUnstructured(t, filenameWidgetClientside)
  856. path := "/namespaces/test/widgets/" + name
  857. verifiedPatch := false
  858. for _, fn := range testingOpenAPISchemaFns {
  859. t.Run("test apply works correctly with unstructured objects", func(t *testing.T) {
  860. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  861. defer tf.Cleanup()
  862. tf.UnstructuredClient = &fake.RESTClient{
  863. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  864. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  865. switch p, m := req.URL.Path, req.Method; {
  866. case p == path && m == "GET":
  867. body := ioutil.NopCloser(bytes.NewReader(curr))
  868. return &http.Response{
  869. StatusCode: 200,
  870. Header: cmdtesting.DefaultHeader(),
  871. Body: body}, nil
  872. case p == path && m == "PATCH":
  873. contentType := req.Header.Get("Content-Type")
  874. if contentType != "application/merge-patch+json" {
  875. t.Fatalf("Unexpected Content-Type: %s", contentType)
  876. }
  877. validatePatchApplication(t, req)
  878. verifiedPatch = true
  879. body := ioutil.NopCloser(bytes.NewReader(curr))
  880. return &http.Response{
  881. StatusCode: 200,
  882. Header: cmdtesting.DefaultHeader(),
  883. Body: body}, nil
  884. default:
  885. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  886. return nil, nil
  887. }
  888. }),
  889. }
  890. tf.OpenAPISchemaFunc = fn
  891. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  892. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  893. cmd := NewCmdApply("kubectl", tf, ioStreams)
  894. cmd.Flags().Set("filename", filenameWidgetClientside)
  895. cmd.Flags().Set("output", "name")
  896. cmd.Run(cmd, []string{})
  897. expected := "widget.unit-test.test.com/" + name + "\n"
  898. if buf.String() != expected {
  899. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
  900. }
  901. if errBuf.String() != "" {
  902. t.Fatalf("unexpected error output: %s", errBuf.String())
  903. }
  904. if !verifiedPatch {
  905. t.Fatal("No server-side patch call detected")
  906. }
  907. })
  908. }
  909. }
  910. // TestUnstructuredIdempotentApply checks repeated apply operation on an unstructured object
  911. func TestUnstructuredIdempotentApply(t *testing.T) {
  912. cmdtesting.InitTestErrorHandler(t)
  913. serversideObject := readUnstructuredFromFile(t, filenameWidgetServerside)
  914. serversideData, err := runtime.Encode(unstructured.JSONFallbackEncoder{Encoder: codec}, serversideObject)
  915. if err != nil {
  916. t.Fatal(err)
  917. }
  918. path := "/namespaces/test/widgets/widget"
  919. for _, fn := range testingOpenAPISchemaFns {
  920. t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) {
  921. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  922. defer tf.Cleanup()
  923. tf.UnstructuredClient = &fake.RESTClient{
  924. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  925. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  926. switch p, m := req.URL.Path, req.Method; {
  927. case p == path && m == "GET":
  928. body := ioutil.NopCloser(bytes.NewReader(serversideData))
  929. return &http.Response{
  930. StatusCode: 200,
  931. Header: cmdtesting.DefaultHeader(),
  932. Body: body}, nil
  933. case p == path && m == "PATCH":
  934. // In idempotent updates, kubectl will resolve to an empty patch and not send anything to the server
  935. // Thus, if we reach this branch, kubectl is unnecessarily sending a patch.
  936. patch, err := ioutil.ReadAll(req.Body)
  937. if err != nil {
  938. t.Fatal(err)
  939. }
  940. t.Fatalf("Unexpected Patch: %s", patch)
  941. return nil, nil
  942. default:
  943. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  944. return nil, nil
  945. }
  946. }),
  947. }
  948. tf.OpenAPISchemaFunc = fn
  949. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  950. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  951. cmd := NewCmdApply("kubectl", tf, ioStreams)
  952. cmd.Flags().Set("filename", filenameWidgetClientside)
  953. cmd.Flags().Set("output", "name")
  954. cmd.Run(cmd, []string{})
  955. expected := "widget.unit-test.test.com/widget\n"
  956. if buf.String() != expected {
  957. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
  958. }
  959. if errBuf.String() != "" {
  960. t.Fatalf("unexpected error output: %s", errBuf.String())
  961. }
  962. })
  963. }
  964. }
  965. func TestRunApplySetLastApplied(t *testing.T) {
  966. cmdtesting.InitTestErrorHandler(t)
  967. nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
  968. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  969. noExistRC, _ := readAndAnnotateReplicationController(t, filenameNoExistRC)
  970. noExistPath := "/namespaces/test/replicationcontrollers/" + noExistRC
  971. noAnnotationName, noAnnotationRC := readReplicationController(t, filenameRCNoAnnotation)
  972. noAnnotationPath := "/namespaces/test/replicationcontrollers/" + noAnnotationName
  973. tests := []struct {
  974. name, nameRC, pathRC, filePath, expectedErr, expectedOut, output string
  975. }{
  976. {
  977. name: "set with exist object",
  978. filePath: filenameRC,
  979. expectedErr: "",
  980. expectedOut: "replicationcontroller/test-rc\n",
  981. output: "name",
  982. },
  983. {
  984. name: "set with no-exist object",
  985. filePath: filenameNoExistRC,
  986. expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-exist)",
  987. expectedOut: "",
  988. output: "name",
  989. },
  990. {
  991. name: "set for the annotation does not exist on the live object",
  992. filePath: filenameRCNoAnnotation,
  993. expectedErr: "error: no last-applied-configuration annotation found on resource: no-annotation, to create the annotation, run the command with --create-annotation",
  994. expectedOut: "",
  995. output: "name",
  996. },
  997. {
  998. name: "set with exist object output json",
  999. filePath: filenameRCJSON,
  1000. expectedErr: "",
  1001. expectedOut: "replicationcontroller/test-rc\n",
  1002. output: "name",
  1003. },
  1004. {
  1005. name: "set test for a directory of files",
  1006. filePath: dirName,
  1007. expectedErr: "",
  1008. expectedOut: "replicationcontroller/test-rc\nreplicationcontroller/test-rc\n",
  1009. output: "name",
  1010. },
  1011. }
  1012. for _, test := range tests {
  1013. t.Run(test.name, func(t *testing.T) {
  1014. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  1015. defer tf.Cleanup()
  1016. tf.UnstructuredClient = &fake.RESTClient{
  1017. GroupVersion: schema.GroupVersion{Version: "v1"},
  1018. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  1019. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  1020. switch p, m := req.URL.Path, req.Method; {
  1021. case p == pathRC && m == "GET":
  1022. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  1023. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  1024. case p == noAnnotationPath && m == "GET":
  1025. bodyRC := ioutil.NopCloser(bytes.NewReader(noAnnotationRC))
  1026. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  1027. case p == noExistPath && m == "GET":
  1028. return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil
  1029. case p == pathRC && m == "PATCH":
  1030. checkPatchString(t, req)
  1031. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  1032. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  1033. case p == "/api/v1/namespaces/test" && m == "GET":
  1034. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil
  1035. default:
  1036. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  1037. return nil, nil
  1038. }
  1039. }),
  1040. }
  1041. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  1042. cmdutil.BehaviorOnFatal(func(str string, code int) {
  1043. if str != test.expectedErr {
  1044. t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr)
  1045. }
  1046. })
  1047. ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
  1048. cmd := NewCmdApplySetLastApplied(tf, ioStreams)
  1049. cmd.Flags().Set("filename", test.filePath)
  1050. cmd.Flags().Set("output", test.output)
  1051. cmd.Run(cmd, []string{})
  1052. if buf.String() != test.expectedOut {
  1053. t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut)
  1054. }
  1055. })
  1056. }
  1057. cmdutil.BehaviorOnFatal(func(str string, code int) {})
  1058. }
  1059. func checkPatchString(t *testing.T, req *http.Request) {
  1060. checkString := string(readBytesFromFile(t, filenameRCPatchTest))
  1061. patch, err := ioutil.ReadAll(req.Body)
  1062. if err != nil {
  1063. t.Fatal(err)
  1064. }
  1065. patchMap := map[string]interface{}{}
  1066. if err := json.Unmarshal(patch, &patchMap); err != nil {
  1067. t.Fatal(err)
  1068. }
  1069. annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
  1070. if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok {
  1071. t.Fatalf("patch does not contain annotation:\n%s\n", patch)
  1072. }
  1073. resultString := annotationsMap["kubectl.kubernetes.io/last-applied-configuration"]
  1074. if resultString != checkString {
  1075. t.Fatalf("patch annotation is not correct, expect:%s\n but got:%s\n", checkString, resultString)
  1076. }
  1077. }
  1078. func TestForceApply(t *testing.T) {
  1079. cmdtesting.InitTestErrorHandler(t)
  1080. scheme := runtime.NewScheme()
  1081. nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
  1082. pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
  1083. pathRCList := "/namespaces/test/replicationcontrollers"
  1084. expected := map[string]int{
  1085. "getOk": 6,
  1086. "getNotFound": 1,
  1087. "getList": 0,
  1088. "patch": 6,
  1089. "delete": 1,
  1090. "post": 1,
  1091. }
  1092. for _, fn := range testingOpenAPISchemaFns {
  1093. t.Run("test apply with --force", func(t *testing.T) {
  1094. deleted := false
  1095. isScaledDownToZero := false
  1096. counts := map[string]int{}
  1097. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  1098. defer tf.Cleanup()
  1099. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  1100. tf.UnstructuredClient = &fake.RESTClient{
  1101. NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
  1102. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  1103. switch p, m := req.URL.Path, req.Method; {
  1104. case strings.HasSuffix(p, pathRC) && m == "GET":
  1105. if deleted {
  1106. counts["getNotFound"]++
  1107. return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
  1108. }
  1109. counts["getOk"]++
  1110. var bodyRC io.ReadCloser
  1111. if isScaledDownToZero {
  1112. rcObj := readReplicationControllerFromFile(t, filenameRC)
  1113. rcObj.Spec.Replicas = utilpointer.Int32Ptr(0)
  1114. rcBytes, err := runtime.Encode(codec, rcObj)
  1115. if err != nil {
  1116. t.Fatal(err)
  1117. }
  1118. bodyRC = ioutil.NopCloser(bytes.NewReader(rcBytes))
  1119. } else {
  1120. bodyRC = ioutil.NopCloser(bytes.NewReader(currentRC))
  1121. }
  1122. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  1123. case strings.HasSuffix(p, pathRCList) && m == "GET":
  1124. counts["getList"]++
  1125. rcObj := readUnstructuredFromFile(t, filenameRC)
  1126. list := &unstructured.UnstructuredList{
  1127. Object: map[string]interface{}{
  1128. "apiVersion": "v1",
  1129. "kind": "ReplicationControllerList",
  1130. },
  1131. Items: []unstructured.Unstructured{*rcObj},
  1132. }
  1133. listBytes, err := runtime.Encode(codec, list)
  1134. if err != nil {
  1135. t.Fatal(err)
  1136. }
  1137. bodyRCList := ioutil.NopCloser(bytes.NewReader(listBytes))
  1138. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRCList}, nil
  1139. case strings.HasSuffix(p, pathRC) && m == "PATCH":
  1140. counts["patch"]++
  1141. if counts["patch"] <= 6 {
  1142. statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first"))
  1143. bodyBytes, _ := json.Marshal(statusErr)
  1144. bodyErr := ioutil.NopCloser(bytes.NewReader(bodyBytes))
  1145. return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil
  1146. }
  1147. t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req)
  1148. return nil, nil
  1149. case strings.HasSuffix(p, pathRC) && m == "PUT":
  1150. counts["put"]++
  1151. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  1152. isScaledDownToZero = true
  1153. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  1154. case strings.HasSuffix(p, pathRCList) && m == "POST":
  1155. counts["post"]++
  1156. deleted = false
  1157. isScaledDownToZero = false
  1158. bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
  1159. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
  1160. default:
  1161. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  1162. return nil, nil
  1163. }
  1164. }),
  1165. }
  1166. fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
  1167. fakeDynamicClient.PrependReactor("delete", "replicationcontrollers", func(action clienttesting.Action) (bool, runtime.Object, error) {
  1168. if deleteAction, ok := action.(clienttesting.DeleteAction); ok {
  1169. if deleteAction.GetName() == nameRC {
  1170. counts["delete"]++
  1171. deleted = true
  1172. return true, nil, nil
  1173. }
  1174. }
  1175. return false, nil, nil
  1176. })
  1177. tf.FakeDynamicClient = fakeDynamicClient
  1178. tf.OpenAPISchemaFunc = fn
  1179. tf.Client = tf.UnstructuredClient
  1180. tf.ClientConfigVal = &restclient.Config{}
  1181. ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
  1182. cmd := NewCmdApply("kubectl", tf, ioStreams)
  1183. cmd.Flags().Set("filename", filenameRC)
  1184. cmd.Flags().Set("output", "name")
  1185. cmd.Flags().Set("force", "true")
  1186. cmd.Run(cmd, []string{})
  1187. for method, exp := range expected {
  1188. if exp != counts[method] {
  1189. t.Errorf("Unexpected amount of %q API calls, wanted %v got %v", method, exp, counts[method])
  1190. }
  1191. }
  1192. if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected {
  1193. t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
  1194. }
  1195. if errBuf.String() != "" {
  1196. t.Fatalf("unexpected error output: %s", errBuf.String())
  1197. }
  1198. })
  1199. }
  1200. }
  1201. func TestDryRunVerifier(t *testing.T) {
  1202. dryRunVerifier := DryRunVerifier{
  1203. Finder: cmdutil.NewCRDFinder(func() ([]schema.GroupKind, error) {
  1204. return []schema.GroupKind{
  1205. {
  1206. Group: "crd.com",
  1207. Kind: "MyCRD",
  1208. },
  1209. {
  1210. Group: "crd.com",
  1211. Kind: "MyNewCRD",
  1212. },
  1213. }, nil
  1214. }),
  1215. OpenAPIGetter: &fakeSchema,
  1216. }
  1217. err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "NodeProxyOptions"})
  1218. if err == nil {
  1219. t.Fatalf("NodeProxyOptions doesn't support dry-run, yet no error found")
  1220. }
  1221. err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"})
  1222. if err != nil {
  1223. t.Fatalf("Pod should support dry-run: %v", err)
  1224. }
  1225. err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"})
  1226. if err != nil {
  1227. t.Fatalf("MyCRD should support dry-run: %v", err)
  1228. }
  1229. err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "Random"})
  1230. if err == nil {
  1231. t.Fatalf("Random doesn't support dry-run, yet no error found")
  1232. }
  1233. }
  1234. type EmptyOpenAPI struct{}
  1235. func (EmptyOpenAPI) OpenAPISchema() (*openapi_v2.Document, error) {
  1236. return &openapi_v2.Document{}, nil
  1237. }
  1238. func TestDryRunVerifierNoOpenAPI(t *testing.T) {
  1239. dryRunVerifier := DryRunVerifier{
  1240. Finder: cmdutil.NewCRDFinder(func() ([]schema.GroupKind, error) {
  1241. return []schema.GroupKind{
  1242. {
  1243. Group: "crd.com",
  1244. Kind: "MyCRD",
  1245. },
  1246. {
  1247. Group: "crd.com",
  1248. Kind: "MyNewCRD",
  1249. },
  1250. }, nil
  1251. }),
  1252. OpenAPIGetter: EmptyOpenAPI{},
  1253. }
  1254. err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"})
  1255. if err == nil {
  1256. t.Fatalf("Pod doesn't support dry-run, yet no error found")
  1257. }
  1258. err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"})
  1259. if err == nil {
  1260. t.Fatalf("MyCRD doesn't support dry-run, yet no error found")
  1261. }
  1262. }