e2e-aci_test.go 17 KB

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