e2e-aci_test.go 16 KB

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