e2e-aci_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. /*
  2. Copyright 2020 Docker, Inc.
  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 main
  14. import (
  15. "context"
  16. "fmt"
  17. "math/rand"
  18. "net/url"
  19. "os"
  20. "strings"
  21. "testing"
  22. "time"
  23. "github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
  24. azure_storage "github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/storage/mgmt/storage"
  25. "github.com/Azure/azure-storage-file-go/azfile"
  26. "github.com/Azure/go-autorest/autorest/to"
  27. . "github.com/onsi/gomega"
  28. log "github.com/sirupsen/logrus"
  29. "github.com/stretchr/testify/suite"
  30. "github.com/docker/api/azure"
  31. "github.com/docker/api/azure/login"
  32. "github.com/docker/api/context/store"
  33. "github.com/docker/api/tests/aci-e2e/storage"
  34. . "github.com/docker/api/tests/framework"
  35. )
  36. const (
  37. location = "westeurope"
  38. contextName = "acitest"
  39. testContainerName = "testcontainername"
  40. testShareName = "dockertestshare"
  41. testFileContent = "Volume mounted with success!"
  42. testFileName = "index.html"
  43. )
  44. var (
  45. subscriptionID string
  46. )
  47. type E2eACISuite struct {
  48. Suite
  49. }
  50. func (s *E2eACISuite) TestACIRunSingleContainer() {
  51. resourceGroupName := s.setupTestResourceGroup()
  52. defer deleteResourceGroup(resourceGroupName)
  53. var nginxExposedURL string
  54. s.Step("runs nginx on port 80", func() {
  55. aciContext := store.AciContext{
  56. SubscriptionID: subscriptionID,
  57. Location: location,
  58. ResourceGroup: resourceGroupName,
  59. }
  60. testStorageAccountName := "storageteste2e" + RandStringBytes(6) // "between 3 and 24 characters in length and use numbers and lower-case letters only"
  61. createStorageAccount(aciContext, testStorageAccountName)
  62. defer deleteStorageAccount(aciContext, testStorageAccountName)
  63. keys := getStorageKeys(aciContext, testStorageAccountName)
  64. firstKey := *keys[0].Value
  65. credential, u := createFileShare(firstKey, testShareName, testStorageAccountName)
  66. uploadFile(credential, u.String(), testFileName, testFileContent)
  67. mountTarget := "/usr/share/nginx/html"
  68. output := s.NewDockerCommand("run", "-d", "nginx",
  69. "-v", fmt.Sprintf("%s:%s@%s:%s",
  70. testStorageAccountName, firstKey, testShareName, mountTarget),
  71. "-p", "80:80",
  72. "--name", testContainerName).ExecOrDie()
  73. Expect(output).To(ContainSubstring(testContainerName))
  74. output = s.NewDockerCommand("ps").ExecOrDie()
  75. lines := Lines(output)
  76. Expect(len(lines)).To(Equal(2))
  77. containerFields := Columns(lines[1])
  78. Expect(containerFields[1]).To(Equal("nginx"))
  79. Expect(containerFields[2]).To(Equal("Running"))
  80. exposedIP := containerFields[3]
  81. containerID := containerFields[0]
  82. Expect(exposedIP).To(ContainSubstring(":80->80/tcp"))
  83. nginxExposedURL = strings.ReplaceAll(exposedIP, "->80/tcp", "")
  84. output = s.NewCommand("curl", nginxExposedURL).ExecOrDie()
  85. Expect(output).To(ContainSubstring(testFileContent))
  86. output = s.NewDockerCommand("logs", containerID).ExecOrDie()
  87. Expect(output).To(ContainSubstring("GET"))
  88. })
  89. s.Step("exec command", func() {
  90. _, err := s.NewDockerCommand("exec", testContainerName, "echo", "fail_with_argument").Exec()
  91. Expect(err.Error()).To(ContainSubstring("ACI exec command does not accept arguments to the command. " +
  92. "Only the binary should be specified"))
  93. })
  94. s.Step("follow logs from nginx", func() {
  95. timeChan := make(chan time.Time)
  96. ctx := s.NewDockerCommand("logs", "--follow", testContainerName).WithTimeout(timeChan)
  97. outChan := make(chan string)
  98. go func() {
  99. output, _ := ctx.Exec()
  100. outChan <- output
  101. }()
  102. s.NewCommand("curl", nginxExposedURL+"/test").ExecOrDie()
  103. // Give the `logs --follow` a little time to get logs of the curl call
  104. time.Sleep(10 * time.Second)
  105. // Trigger a timeout to make ctx.Exec exit
  106. timeChan <- time.Now()
  107. output := <-outChan
  108. Expect(output).To(ContainSubstring("/test"))
  109. })
  110. s.Step("removes container nginx", func() {
  111. output := s.NewDockerCommand("rm", testContainerName).ExecOrDie()
  112. Expect(Lines(output)[0]).To(Equal(testContainerName))
  113. })
  114. s.Step("re-run nginx with modified cpu/mem, and without --detach and follow logs", func() {
  115. shutdown := make(chan time.Time)
  116. errs := make(chan error)
  117. outChan := make(chan string)
  118. cmd := s.NewDockerCommand("run", "nginx", "--memory", "0.1G", "--cpus", "0.1", "-p", "80:80", "--name", testContainerName).WithTimeout(shutdown)
  119. go func() {
  120. output, err := cmd.Exec()
  121. outChan <- output
  122. errs <- err
  123. }()
  124. var containerID string
  125. err := WaitFor(time.Second, 100*time.Second, errs, func() bool {
  126. output := s.NewDockerCommand("ps").ExecOrDie()
  127. lines := Lines(output)
  128. if len(lines) != 2 {
  129. return false
  130. }
  131. containerFields := Columns(lines[1])
  132. if containerFields[2] != "Running" {
  133. return false
  134. }
  135. containerID = containerFields[0]
  136. nginxExposedURL = strings.ReplaceAll(containerFields[3], "->80/tcp", "")
  137. return true
  138. })
  139. Expect(err).NotTo(HaveOccurred())
  140. s.NewCommand("curl", nginxExposedURL+"/test").ExecOrDie()
  141. inspect := s.NewDockerCommand("inspect", containerID).ExecOrDie()
  142. Expect(inspect).To(ContainSubstring("\"CPULimit\": 0.1"))
  143. Expect(inspect).To(ContainSubstring("\"MemoryLimit\": 107374182"))
  144. // Give a little time to get logs of the curl call
  145. time.Sleep(5 * time.Second)
  146. // Kill
  147. close(shutdown)
  148. output := <-outChan
  149. Expect(output).To(ContainSubstring("/test"))
  150. })
  151. s.Step("removes container nginx", func() {
  152. output := s.NewDockerCommand("rm", testContainerName).ExecOrDie()
  153. Expect(Lines(output)[0]).To(Equal(testContainerName))
  154. })
  155. }
  156. func (s *E2eACISuite) TestACIComposeApplication() {
  157. defer deleteResourceGroup(s.setupTestResourceGroup())
  158. var exposedURL string
  159. const composeFile = "../composefiles/aci-demo/aci_demo_port.yaml"
  160. const composeFileMultiplePorts = "../composefiles/aci-demo/aci_demo_multi_port.yaml"
  161. const composeProjectName = "acie2e"
  162. const serverContainer = composeProjectName + "_web"
  163. const wordsContainer = composeProjectName + "_words"
  164. s.Step("deploys a compose app", func() {
  165. // specifically do not specify project name here, it will be derived from current folder "acie2e"
  166. s.NewDockerCommand("compose", "up", "-f", composeFile).ExecOrDie()
  167. output := s.NewDockerCommand("ps").ExecOrDie()
  168. Lines := Lines(output)
  169. Expect(len(Lines)).To(Equal(4))
  170. webChecked := false
  171. for _, line := range Lines[1:] {
  172. Expect(line).To(ContainSubstring("Running"))
  173. if strings.Contains(line, serverContainer) {
  174. webChecked = true
  175. containerFields := Columns(line)
  176. exposedIP := containerFields[3]
  177. Expect(exposedIP).To(ContainSubstring(":80->80/tcp"))
  178. exposedURL = strings.ReplaceAll(exposedIP, "->80/tcp", "")
  179. output = s.NewCommand("curl", exposedURL).ExecOrDie()
  180. Expect(output).To(ContainSubstring("Docker Compose demo"))
  181. output = s.NewCommand("curl", exposedURL+"/words/noun").ExecOrDie()
  182. Expect(output).To(ContainSubstring("\"word\":"))
  183. }
  184. }
  185. Expect(webChecked).To(BeTrue())
  186. })
  187. s.Step("get logs from web service", func() {
  188. output := s.NewDockerCommand("logs", serverContainer).ExecOrDie()
  189. Expect(output).To(ContainSubstring("Listening on port 80"))
  190. })
  191. s.Step("updates a compose app", func() {
  192. s.NewDockerCommand("compose", "up", "-f", composeFileMultiplePorts, "--project-name", composeProjectName).ExecOrDie()
  193. // Expect(output).To(ContainSubstring("Successfully deployed"))
  194. output := s.NewDockerCommand("ps").ExecOrDie()
  195. Lines := Lines(output)
  196. Expect(len(Lines)).To(Equal(4))
  197. webChecked := false
  198. wordsChecked := false
  199. for _, line := range Lines[1:] {
  200. Expect(line).To(ContainSubstring("Running"))
  201. if strings.Contains(line, serverContainer) {
  202. webChecked = true
  203. containerFields := Columns(line)
  204. exposedIP := containerFields[3]
  205. Expect(exposedIP).To(ContainSubstring(":80->80/tcp"))
  206. url := strings.ReplaceAll(exposedIP, "->80/tcp", "")
  207. Expect(exposedURL).To(Equal(url))
  208. }
  209. if strings.Contains(line, wordsContainer) {
  210. wordsChecked = true
  211. containerFields := Columns(line)
  212. exposedIP := containerFields[3]
  213. Expect(exposedIP).To(ContainSubstring(":8080->8080/tcp"))
  214. url := strings.ReplaceAll(exposedIP, "->8080/tcp", "")
  215. output = s.NewCommand("curl", url+"/noun").ExecOrDie()
  216. Expect(output).To(ContainSubstring("\"word\":"))
  217. }
  218. }
  219. Expect(webChecked).To(BeTrue())
  220. Expect(wordsChecked).To(BeTrue())
  221. })
  222. s.Step("shutdown compose app", func() {
  223. s.NewDockerCommand("compose", "down", "--project-name", composeProjectName).ExecOrDie()
  224. })
  225. }
  226. func (s *E2eACISuite) TestACIDeployMySQlwithEnvVars() {
  227. defer deleteResourceGroup(s.setupTestResourceGroup())
  228. s.Step("runs mysql with env variables", func() {
  229. err := os.Setenv("MYSQL_USER", "user1")
  230. Expect(err).To(BeNil())
  231. s.NewDockerCommand("run", "-d", "mysql:5.7", "-e", "MYSQL_ROOT_PASSWORD=rootpwd", "-e", "MYSQL_DATABASE=mytestdb", "-e", "MYSQL_USER", "-e", "MYSQL_PASSWORD=userpwd").ExecOrDie()
  232. output := s.NewDockerCommand("ps").ExecOrDie()
  233. lines := Lines(output)
  234. Expect(len(lines)).To(Equal(2))
  235. containerFields := Columns(lines[1])
  236. containerID := containerFields[0]
  237. Expect(containerFields[1]).To(Equal("mysql:5.7"))
  238. Expect(containerFields[2]).To(Equal("Running"))
  239. errs := make(chan error)
  240. err = WaitFor(time.Second, 100*time.Second, errs, func() bool {
  241. output = s.NewDockerCommand("logs", containerID).ExecOrDie()
  242. return strings.Contains(output, "Giving user user1 access to schema mytestdb")
  243. })
  244. Expect(err).To(BeNil())
  245. })
  246. s.Step("switches back to default context", func() {
  247. output := s.NewCommand("docker", "context", "use", "default").ExecOrDie()
  248. Expect(output).To(ContainSubstring("default"))
  249. })
  250. s.Step("deletes test context", func() {
  251. output := s.NewCommand("docker", "context", "rm", contextName).ExecOrDie()
  252. Expect(output).To(ContainSubstring(contextName))
  253. })
  254. }
  255. func (s *E2eACISuite) setupTestResourceGroup() string {
  256. var resourceGroupName = randomResourceGroup()
  257. s.Step("should be initialized with default context", s.checkDefaultContext)
  258. s.Step("Logs in azure using service principal credentials", azureLogin)
  259. s.Step("creates a new aci context for tests and use it", s.createAciContextAndUseIt(resourceGroupName))
  260. s.Step("ensures no container is running initially", s.checkNoContainnersRunning)
  261. return resourceGroupName
  262. }
  263. func (s *E2eACISuite) checkDefaultContext() {
  264. output := s.NewCommand("docker", "context", "ls").ExecOrDie()
  265. Expect(output).To(Not(ContainSubstring(contextName)))
  266. Expect(output).To(ContainSubstring("default *"))
  267. }
  268. func azureLogin() {
  269. login, err := login.NewAzureLoginService()
  270. Expect(err).To(BeNil())
  271. // in order to create new service principal and get these 3 values : `az ad sp create-for-rbac --name 'TestServicePrincipal' --sdk-auth`
  272. clientID := os.Getenv("AZURE_CLIENT_ID")
  273. clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
  274. tenantID := os.Getenv("AZURE_TENANT_ID")
  275. err = login.TestLoginFromServicePrincipal(clientID, clientSecret, tenantID)
  276. Expect(err).To(BeNil())
  277. }
  278. func (s *E2eACISuite) createAciContextAndUseIt(resourceGroupName string) func() {
  279. return func() {
  280. setupTestResourceGroup(resourceGroupName)
  281. helper := azure.NewACIResourceGroupHelper()
  282. models, err := helper.GetSubscriptionIDs(context.TODO())
  283. Expect(err).To(BeNil())
  284. subscriptionID = *models[0].SubscriptionID
  285. s.NewDockerCommand("context", "create", "aci", contextName, "--subscription-id", subscriptionID, "--resource-group", resourceGroupName, "--location", location).ExecOrDie()
  286. currentContext := s.NewCommand("docker", "context", "use", contextName).ExecOrDie()
  287. Expect(currentContext).To(ContainSubstring(contextName))
  288. output := s.NewCommand("docker", "context", "ls").ExecOrDie()
  289. Expect(output).To(ContainSubstring("acitest *"))
  290. }
  291. }
  292. func (s *E2eACISuite) checkNoContainnersRunning() {
  293. output := s.NewDockerCommand("ps").ExecOrDie()
  294. Expect(len(Lines(output))).To(Equal(1))
  295. }
  296. func randomResourceGroup() string {
  297. return "resourceGroupTestE2E-" + RandStringBytes(10)
  298. }
  299. func createStorageAccount(aciContext store.AciContext, accountName string) azure_storage.Account {
  300. log.Println("Creating storage account " + accountName)
  301. storageAccount, err := storage.CreateStorageAccount(context.TODO(), aciContext, accountName)
  302. Expect(err).To(BeNil())
  303. Expect(*storageAccount.Name).To(Equal(accountName))
  304. return storageAccount
  305. }
  306. func getStorageKeys(aciContext store.AciContext, storageAccountName string) []azure_storage.AccountKey {
  307. list, err := storage.ListKeys(context.TODO(), aciContext, storageAccountName)
  308. Expect(err).To(BeNil())
  309. Expect(list.Keys).ToNot(BeNil())
  310. Expect(len(*list.Keys)).To(BeNumerically(">", 0))
  311. return *list.Keys
  312. }
  313. func deleteStorageAccount(aciContext store.AciContext, testStorageAccountName string) {
  314. log.Println("Deleting storage account " + testStorageAccountName)
  315. _, err := storage.DeleteStorageAccount(context.TODO(), aciContext, testStorageAccountName)
  316. Expect(err).To(BeNil())
  317. }
  318. func createFileShare(key, shareName string, testStorageAccountName string) (azfile.SharedKeyCredential, url.URL) {
  319. // Create a ShareURL object that wraps a soon-to-be-created share's URL and a default pipeline.
  320. u, _ := url.Parse(fmt.Sprintf("https://%s.file.core.windows.net/%s", testStorageAccountName, shareName))
  321. credential, err := azfile.NewSharedKeyCredential(testStorageAccountName, key)
  322. Expect(err).To(BeNil())
  323. shareURL := azfile.NewShareURL(*u, azfile.NewPipeline(credential, azfile.PipelineOptions{}))
  324. _, err = shareURL.Create(context.TODO(), azfile.Metadata{}, 0)
  325. Expect(err).To(BeNil())
  326. return *credential, *u
  327. }
  328. func uploadFile(credential azfile.SharedKeyCredential, baseURL, fileName, fileContent string) {
  329. fURL, err := url.Parse(baseURL + "/" + fileName)
  330. Expect(err).To(BeNil())
  331. fileURL := azfile.NewFileURL(*fURL, azfile.NewPipeline(&credential, azfile.PipelineOptions{}))
  332. err = azfile.UploadBufferToAzureFile(context.TODO(), []byte(fileContent), fileURL, azfile.UploadToAzureFileOptions{})
  333. Expect(err).To(BeNil())
  334. }
  335. func TestE2eACI(t *testing.T) {
  336. suite.Run(t, new(E2eACISuite))
  337. }
  338. func setupTestResourceGroup(resourceGroupName string) {
  339. log.Println("Creating resource group " + resourceGroupName)
  340. ctx := context.TODO()
  341. helper := azure.NewACIResourceGroupHelper()
  342. models, err := helper.GetSubscriptionIDs(ctx)
  343. Expect(err).To(BeNil())
  344. _, err = helper.CreateOrUpdate(ctx, *models[0].SubscriptionID, resourceGroupName, resources.Group{
  345. Location: to.StringPtr(location),
  346. })
  347. Expect(err).To(BeNil())
  348. }
  349. func deleteResourceGroup(resourceGroupName string) {
  350. log.Println("Deleting resource group " + resourceGroupName)
  351. ctx := context.TODO()
  352. helper := azure.NewACIResourceGroupHelper()
  353. models, err := helper.GetSubscriptionIDs(ctx)
  354. Expect(err).To(BeNil())
  355. err = helper.DeleteAsync(ctx, *models[0].SubscriptionID, resourceGroupName)
  356. Expect(err).To(BeNil())
  357. }
  358. func RandStringBytes(n int) string {
  359. rand.Seed(time.Now().UnixNano())
  360. const digits = "0123456789"
  361. b := make([]byte, n)
  362. for i := range b {
  363. b[i] = digits[rand.Intn(len(digits))]
  364. }
  365. return string(b)
  366. }