addon_update.go 13 KB

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