e2e-aci_test.go 16 KB

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