addon_update.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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 lifecycle
  14. import (
  15. "bytes"
  16. "fmt"
  17. "io"
  18. "os"
  19. "strings"
  20. "time"
  21. "golang.org/x/crypto/ssh"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/labels"
  24. clientset "k8s.io/client-go/kubernetes"
  25. "k8s.io/kubernetes/test/e2e/framework"
  26. e2elog "k8s.io/kubernetes/test/e2e/framework/log"
  27. e2essh "k8s.io/kubernetes/test/e2e/framework/ssh"
  28. "github.com/onsi/ginkgo"
  29. "github.com/onsi/gomega"
  30. imageutils "k8s.io/kubernetes/test/utils/image"
  31. )
  32. // TODO: it would probably be slightly better to build up the objects
  33. // in the code and then serialize to yaml.
  34. var reconcileAddonController = `
  35. apiVersion: v1
  36. kind: ReplicationController
  37. metadata:
  38. name: addon-reconcile-test
  39. namespace: %s
  40. labels:
  41. k8s-app: addon-reconcile-test
  42. kubernetes.io/cluster-service: "true"
  43. addonmanager.kubernetes.io/mode: Reconcile
  44. spec:
  45. replicas: 2
  46. selector:
  47. k8s-app: addon-reconcile-test
  48. template:
  49. metadata:
  50. labels:
  51. k8s-app: addon-reconcile-test
  52. spec:
  53. containers:
  54. - image: %s
  55. name: addon-reconcile-test
  56. ports:
  57. - containerPort: 9376
  58. protocol: TCP
  59. `
  60. // Should update "reconcile" class addon.
  61. var reconcileAddonControllerUpdated = `
  62. apiVersion: v1
  63. kind: ReplicationController
  64. metadata:
  65. name: addon-reconcile-test
  66. namespace: %s
  67. labels:
  68. k8s-app: addon-reconcile-test
  69. kubernetes.io/cluster-service: "true"
  70. addonmanager.kubernetes.io/mode: Reconcile
  71. newLabel: addon-reconcile-test
  72. spec:
  73. replicas: 2
  74. selector:
  75. k8s-app: addon-reconcile-test
  76. template:
  77. metadata:
  78. labels:
  79. k8s-app: addon-reconcile-test
  80. spec:
  81. containers:
  82. - image: %s
  83. name: addon-reconcile-test
  84. ports:
  85. - containerPort: 9376
  86. protocol: TCP
  87. `
  88. var ensureExistsAddonService = `
  89. apiVersion: v1
  90. kind: Service
  91. metadata:
  92. name: addon-ensure-exists-test
  93. namespace: %s
  94. labels:
  95. k8s-app: addon-ensure-exists-test
  96. addonmanager.kubernetes.io/mode: EnsureExists
  97. spec:
  98. ports:
  99. - port: 9376
  100. protocol: TCP
  101. targetPort: 9376
  102. selector:
  103. k8s-app: addon-ensure-exists-test
  104. `
  105. // Should create but don't update "ensure exist" class addon.
  106. var ensureExistsAddonServiceUpdated = `
  107. apiVersion: v1
  108. kind: Service
  109. metadata:
  110. name: addon-ensure-exists-test
  111. namespace: %s
  112. labels:
  113. k8s-app: addon-ensure-exists-test
  114. addonmanager.kubernetes.io/mode: EnsureExists
  115. newLabel: addon-ensure-exists-test
  116. spec:
  117. ports:
  118. - port: 9376
  119. protocol: TCP
  120. targetPort: 9376
  121. selector:
  122. k8s-app: addon-ensure-exists-test
  123. `
  124. var deprecatedLabelAddonService = `
  125. apiVersion: v1
  126. kind: Service
  127. metadata:
  128. name: addon-deprecated-label-test
  129. namespace: %s
  130. labels:
  131. k8s-app: addon-deprecated-label-test
  132. kubernetes.io/cluster-service: "true"
  133. spec:
  134. ports:
  135. - port: 9376
  136. protocol: TCP
  137. targetPort: 9376
  138. selector:
  139. k8s-app: addon-deprecated-label-test
  140. `
  141. // Should update addon with label "kubernetes.io/cluster-service=true".
  142. var deprecatedLabelAddonServiceUpdated = `
  143. apiVersion: v1
  144. kind: Service
  145. metadata:
  146. name: addon-deprecated-label-test
  147. namespace: %s
  148. labels:
  149. k8s-app: addon-deprecated-label-test
  150. kubernetes.io/cluster-service: "true"
  151. newLabel: addon-deprecated-label-test
  152. spec:
  153. ports:
  154. - port: 9376
  155. protocol: TCP
  156. targetPort: 9376
  157. selector:
  158. k8s-app: addon-deprecated-label-test
  159. `
  160. // Should not create addon without valid label.
  161. var invalidAddonController = `
  162. apiVersion: v1
  163. kind: ReplicationController
  164. metadata:
  165. name: invalid-addon-test
  166. namespace: %s
  167. labels:
  168. k8s-app: invalid-addon-test
  169. addonmanager.kubernetes.io/mode: NotMatch
  170. spec:
  171. replicas: 2
  172. selector:
  173. k8s-app: invalid-addon-test
  174. template:
  175. metadata:
  176. labels:
  177. k8s-app: invalid-addon-test
  178. spec:
  179. containers:
  180. - image: %s
  181. name: invalid-addon-test
  182. ports:
  183. - containerPort: 9376
  184. protocol: TCP
  185. `
  186. const (
  187. addonTestPollInterval = 3 * time.Second
  188. addonTestPollTimeout = 5 * time.Minute
  189. addonNsName = metav1.NamespaceSystem
  190. )
  191. var serveHostnameImage = imageutils.GetE2EImage(imageutils.ServeHostname)
  192. type stringPair struct {
  193. data, fileName string
  194. }
  195. var _ = SIGDescribe("Addon update", func() {
  196. var dir string
  197. var sshClient *ssh.Client
  198. f := framework.NewDefaultFramework("addon-update-test")
  199. ginkgo.BeforeEach(func() {
  200. // This test requires:
  201. // - SSH master access
  202. // ... so the provider check should be identical to the intersection of
  203. // providers that provide those capabilities.
  204. if !framework.ProviderIs("gce") {
  205. return
  206. }
  207. var err error
  208. sshClient, err = getMasterSSHClient()
  209. framework.ExpectNoError(err, "Failed to get the master SSH client.")
  210. })
  211. ginkgo.AfterEach(func() {
  212. if sshClient != nil {
  213. sshClient.Close()
  214. }
  215. })
  216. // WARNING: the test is not parallel-friendly!
  217. ginkgo.It("should propagate add-on file changes [Slow]", func() {
  218. // This test requires:
  219. // - SSH
  220. // - master access
  221. // ... so the provider check should be identical to the intersection of
  222. // providers that provide those capabilities.
  223. framework.SkipUnlessProviderIs("gce")
  224. //these tests are long, so I squeezed several cases in one scenario
  225. gomega.Expect(sshClient).NotTo(gomega.BeNil())
  226. dir = f.Namespace.Name // we use it only to give a unique string for each test execution
  227. temporaryRemotePathPrefix := "addon-test-dir"
  228. temporaryRemotePath := temporaryRemotePathPrefix + "/" + dir // in home directory on kubernetes-master
  229. defer sshExec(sshClient, fmt.Sprintf("rm -rf %s", temporaryRemotePathPrefix)) // ignore the result in cleanup
  230. sshExecAndVerify(sshClient, fmt.Sprintf("mkdir -p %s", temporaryRemotePath))
  231. rcAddonReconcile := "addon-reconcile-controller.yaml"
  232. rcAddonReconcileUpdated := "addon-reconcile-controller-Updated.yaml"
  233. rcInvalid := "invalid-addon-controller.yaml"
  234. svcAddonDeprecatedLabel := "addon-deprecated-label-service.yaml"
  235. svcAddonDeprecatedLabelUpdated := "addon-deprecated-label-service-updated.yaml"
  236. svcAddonEnsureExists := "addon-ensure-exists-service.yaml"
  237. svcAddonEnsureExistsUpdated := "addon-ensure-exists-service-updated.yaml"
  238. var remoteFiles []stringPair = []stringPair{
  239. {fmt.Sprintf(reconcileAddonController, addonNsName, serveHostnameImage), rcAddonReconcile},
  240. {fmt.Sprintf(reconcileAddonControllerUpdated, addonNsName, serveHostnameImage), rcAddonReconcileUpdated},
  241. {fmt.Sprintf(deprecatedLabelAddonService, addonNsName), svcAddonDeprecatedLabel},
  242. {fmt.Sprintf(deprecatedLabelAddonServiceUpdated, addonNsName), svcAddonDeprecatedLabelUpdated},
  243. {fmt.Sprintf(ensureExistsAddonService, addonNsName), svcAddonEnsureExists},
  244. {fmt.Sprintf(ensureExistsAddonServiceUpdated, addonNsName), svcAddonEnsureExistsUpdated},
  245. {fmt.Sprintf(invalidAddonController, addonNsName, serveHostnameImage), rcInvalid},
  246. }
  247. for _, p := range remoteFiles {
  248. err := writeRemoteFile(sshClient, p.data, temporaryRemotePath, p.fileName, 0644)
  249. framework.ExpectNoError(err, "Failed to write file %q at remote path %q with ssh client %+v", p.fileName, temporaryRemotePath, sshClient)
  250. }
  251. // directory on kubernetes-master
  252. destinationDirPrefix := "/etc/kubernetes/addons/addon-test-dir"
  253. destinationDir := destinationDirPrefix + "/" + dir
  254. // cleanup from previous tests
  255. _, _, _, err := sshExec(sshClient, fmt.Sprintf("sudo rm -rf %s", destinationDirPrefix))
  256. framework.ExpectNoError(err, "Failed to remove remote dir %q with ssh client %+v", destinationDirPrefix, sshClient)
  257. defer sshExec(sshClient, fmt.Sprintf("sudo rm -rf %s", destinationDirPrefix)) // ignore result in cleanup
  258. sshExecAndVerify(sshClient, fmt.Sprintf("sudo mkdir -p %s", destinationDir))
  259. ginkgo.By("copy invalid manifests to the destination dir")
  260. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, rcInvalid, destinationDir, rcInvalid))
  261. // we will verify at the end of the test that the objects weren't created from the invalid manifests
  262. ginkgo.By("copy new manifests")
  263. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, rcAddonReconcile, destinationDir, rcAddonReconcile))
  264. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonDeprecatedLabel, destinationDir, svcAddonDeprecatedLabel))
  265. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonEnsureExists, destinationDir, svcAddonEnsureExists))
  266. // Delete the "ensure exist class" addon at the end.
  267. defer func() {
  268. e2elog.Logf("Cleaning up ensure exist class addon.")
  269. err := f.ClientSet.CoreV1().Services(addonNsName).Delete("addon-ensure-exists-test", nil)
  270. framework.ExpectNoError(err)
  271. }()
  272. waitForReplicationControllerInAddonTest(f.ClientSet, addonNsName, "addon-reconcile-test", true)
  273. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-deprecated-label-test", true)
  274. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-ensure-exists-test", true)
  275. // Replace the manifests with new contents.
  276. ginkgo.By("update manifests")
  277. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, rcAddonReconcileUpdated, destinationDir, rcAddonReconcile))
  278. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonDeprecatedLabelUpdated, destinationDir, svcAddonDeprecatedLabel))
  279. sshExecAndVerify(sshClient, fmt.Sprintf("sudo cp %s/%s %s/%s", temporaryRemotePath, svcAddonEnsureExistsUpdated, destinationDir, svcAddonEnsureExists))
  280. // Wait for updated addons to have the new added label.
  281. reconcileSelector := labels.SelectorFromSet(labels.Set(map[string]string{"newLabel": "addon-reconcile-test"}))
  282. waitForReplicationControllerwithSelectorInAddonTest(f.ClientSet, addonNsName, true, reconcileSelector)
  283. deprecatedLabelSelector := labels.SelectorFromSet(labels.Set(map[string]string{"newLabel": "addon-deprecated-label-test"}))
  284. waitForServicewithSelectorInAddonTest(f.ClientSet, addonNsName, true, deprecatedLabelSelector)
  285. // "Ensure exist class" addon should not be updated.
  286. ensureExistSelector := labels.SelectorFromSet(labels.Set(map[string]string{"newLabel": "addon-ensure-exists-test"}))
  287. waitForServicewithSelectorInAddonTest(f.ClientSet, addonNsName, false, ensureExistSelector)
  288. ginkgo.By("remove manifests")
  289. sshExecAndVerify(sshClient, fmt.Sprintf("sudo rm %s/%s", destinationDir, rcAddonReconcile))
  290. sshExecAndVerify(sshClient, fmt.Sprintf("sudo rm %s/%s", destinationDir, svcAddonDeprecatedLabel))
  291. sshExecAndVerify(sshClient, fmt.Sprintf("sudo rm %s/%s", destinationDir, svcAddonEnsureExists))
  292. waitForReplicationControllerInAddonTest(f.ClientSet, addonNsName, "addon-reconcile-test", false)
  293. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-deprecated-label-test", false)
  294. // "Ensure exist class" addon will not be deleted when manifest is removed.
  295. waitForServiceInAddonTest(f.ClientSet, addonNsName, "addon-ensure-exists-test", true)
  296. ginkgo.By("verify invalid addons weren't created")
  297. _, err = f.ClientSet.CoreV1().ReplicationControllers(addonNsName).Get("invalid-addon-test", metav1.GetOptions{})
  298. framework.ExpectError(err)
  299. // Invalid addon manifests and the "ensure exist class" addon will be deleted by the deferred function.
  300. })
  301. })
  302. func waitForServiceInAddonTest(c clientset.Interface, addonNamespace, name string, exist bool) {
  303. framework.ExpectNoError(framework.WaitForService(c, addonNamespace, name, exist, addonTestPollInterval, addonTestPollTimeout))
  304. }
  305. func waitForReplicationControllerInAddonTest(c clientset.Interface, addonNamespace, name string, exist bool) {
  306. framework.ExpectNoError(framework.WaitForReplicationController(c, addonNamespace, name, exist, addonTestPollInterval, addonTestPollTimeout))
  307. }
  308. func waitForServicewithSelectorInAddonTest(c clientset.Interface, addonNamespace string, exist bool, selector labels.Selector) {
  309. framework.ExpectNoError(framework.WaitForServiceWithSelector(c, addonNamespace, selector, exist, addonTestPollInterval, addonTestPollTimeout))
  310. }
  311. func waitForReplicationControllerwithSelectorInAddonTest(c clientset.Interface, addonNamespace string, exist bool, selector labels.Selector) {
  312. framework.ExpectNoError(framework.WaitForReplicationControllerwithSelector(c, addonNamespace, selector, exist, addonTestPollInterval,
  313. addonTestPollTimeout))
  314. }
  315. // TODO use the ssh.SSH code, either adding an SCP to it or copying files
  316. // differently.
  317. func getMasterSSHClient() (*ssh.Client, error) {
  318. // Get a signer for the provider.
  319. signer, err := e2essh.GetSigner(framework.TestContext.Provider)
  320. if err != nil {
  321. return nil, fmt.Errorf("error getting signer for provider %s: '%v'", framework.TestContext.Provider, err)
  322. }
  323. sshUser := os.Getenv("KUBE_SSH_USER")
  324. if sshUser == "" {
  325. sshUser = os.Getenv("USER")
  326. }
  327. config := &ssh.ClientConfig{
  328. User: sshUser,
  329. Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
  330. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  331. }
  332. host := framework.GetMasterHost() + ":22"
  333. client, err := ssh.Dial("tcp", host, config)
  334. if err != nil {
  335. return nil, fmt.Errorf("error getting SSH client to host %s: '%v'", host, err)
  336. }
  337. return client, err
  338. }
  339. func sshExecAndVerify(client *ssh.Client, cmd string) {
  340. _, _, rc, err := sshExec(client, cmd)
  341. framework.ExpectNoError(err, "Failed to execute %q with ssh client %+v", cmd, client)
  342. gomega.Expect(rc).To(gomega.Equal(0), "error return code from executing command on the cluster: %s", cmd)
  343. }
  344. func sshExec(client *ssh.Client, cmd string) (string, string, int, error) {
  345. e2elog.Logf("Executing '%s' on %v", cmd, client.RemoteAddr())
  346. session, err := client.NewSession()
  347. if err != nil {
  348. return "", "", 0, fmt.Errorf("error creating session to host %s: '%v'", client.RemoteAddr(), err)
  349. }
  350. defer session.Close()
  351. // Run the command.
  352. code := 0
  353. var bout, berr bytes.Buffer
  354. session.Stdout, session.Stderr = &bout, &berr
  355. err = session.Run(cmd)
  356. if err != nil {
  357. // Check whether the command failed to run or didn't complete.
  358. if exiterr, ok := err.(*ssh.ExitError); ok {
  359. // If we got an ExitError and the exit code is nonzero, we'll
  360. // consider the SSH itself successful (just that the command run
  361. // errored on the host).
  362. if code = exiterr.ExitStatus(); code != 0 {
  363. err = nil
  364. }
  365. } else {
  366. // Some other kind of error happened (e.g. an IOError); consider the
  367. // SSH unsuccessful.
  368. err = fmt.Errorf("failed running `%s` on %s: '%v'", cmd, client.RemoteAddr(), err)
  369. }
  370. }
  371. return bout.String(), berr.String(), code, err
  372. }
  373. func writeRemoteFile(sshClient *ssh.Client, data, dir, fileName string, mode os.FileMode) error {
  374. e2elog.Logf(fmt.Sprintf("Writing remote file '%s/%s' on %v", dir, fileName, sshClient.RemoteAddr()))
  375. session, err := sshClient.NewSession()
  376. if err != nil {
  377. return fmt.Errorf("error creating session to host %s: '%v'", sshClient.RemoteAddr(), err)
  378. }
  379. defer session.Close()
  380. fileSize := len(data)
  381. pipe, err := session.StdinPipe()
  382. if err != nil {
  383. return err
  384. }
  385. defer pipe.Close()
  386. if err := session.Start(fmt.Sprintf("scp -t %s", dir)); err != nil {
  387. return err
  388. }
  389. fmt.Fprintf(pipe, "C%#o %d %s\n", mode, fileSize, fileName)
  390. io.Copy(pipe, strings.NewReader(data))
  391. fmt.Fprint(pipe, "\x00")
  392. pipe.Close()
  393. return session.Wait()
  394. }