e2e-aci_test.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. /*
  2. Copyright 2020 Docker Compose CLI 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 main
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "io/ioutil"
  19. "math/rand"
  20. "net/http"
  21. "net/url"
  22. "os"
  23. "path/filepath"
  24. "runtime"
  25. "strconv"
  26. "strings"
  27. "syscall"
  28. "testing"
  29. "time"
  30. "gotest.tools/v3/assert"
  31. is "gotest.tools/v3/assert/cmp"
  32. "gotest.tools/v3/icmd"
  33. "gotest.tools/v3/poll"
  34. "github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
  35. "github.com/Azure/azure-storage-file-go/azfile"
  36. "github.com/Azure/go-autorest/autorest/to"
  37. "github.com/prometheus/tsdb/fileutil"
  38. "github.com/docker/compose-cli/aci"
  39. "github.com/docker/compose-cli/aci/convert"
  40. "github.com/docker/compose-cli/aci/login"
  41. "github.com/docker/compose-cli/api/containers"
  42. "github.com/docker/compose-cli/context/store"
  43. "github.com/docker/compose-cli/errdefs"
  44. . "github.com/docker/compose-cli/tests/framework"
  45. )
  46. const (
  47. contextName = "aci-test"
  48. )
  49. var (
  50. binDir string
  51. location = []string{"eastus2"}
  52. )
  53. func TestMain(m *testing.M) {
  54. p, cleanup, err := SetupExistingCLI()
  55. if err != nil {
  56. fmt.Println(err)
  57. os.Exit(1)
  58. }
  59. binDir = p
  60. exitCode := m.Run()
  61. cleanup()
  62. os.Exit(exitCode)
  63. }
  64. // Cannot be parallelized as login/logout is global.
  65. func TestLoginLogout(t *testing.T) {
  66. startTime := strconv.Itoa(int(time.Now().UnixNano()))
  67. c := NewE2eCLI(t, binDir)
  68. rg := "E2E-" + startTime
  69. t.Run("login", func(t *testing.T) {
  70. azureLogin(t, c)
  71. })
  72. t.Run("create context", func(t *testing.T) {
  73. sID := getSubscriptionID(t)
  74. location := getTestLocation()
  75. err := createResourceGroup(t, sID, rg, location)
  76. assert.Check(t, is.Nil(err))
  77. t.Cleanup(func() {
  78. _ = deleteResourceGroup(t, rg)
  79. })
  80. c.RunDockerCmd("context", "create", "aci", contextName, "--subscription-id", sID, "--resource-group", rg, "--location", location)
  81. res := c.RunDockerCmd("context", "use", contextName)
  82. res.Assert(t, icmd.Expected{Out: contextName})
  83. res = c.RunDockerCmd("context", "ls")
  84. res.Assert(t, icmd.Expected{Out: contextName + " *"})
  85. })
  86. t.Run("delete context", func(t *testing.T) {
  87. res := c.RunDockerCmd("context", "use", "default")
  88. res.Assert(t, icmd.Expected{Out: "default"})
  89. res = c.RunDockerCmd("context", "rm", contextName)
  90. res.Assert(t, icmd.Expected{Out: contextName})
  91. })
  92. t.Run("logout", func(t *testing.T) {
  93. _, err := os.Stat(login.GetTokenStorePath())
  94. assert.NilError(t, err)
  95. res := c.RunDockerCmd("logout", "azure")
  96. res.Assert(t, icmd.Expected{Out: "Removing login credentials for Azure"})
  97. _, err = os.Stat(login.GetTokenStorePath())
  98. errMsg := "no such file or directory"
  99. if runtime.GOOS == "windows" {
  100. errMsg = "The system cannot find the file specified"
  101. }
  102. assert.ErrorContains(t, err, errMsg)
  103. })
  104. t.Run("create context fail", func(t *testing.T) {
  105. res := c.RunDockerOrExitError("context", "create", "aci", "fail-context")
  106. res.Assert(t, icmd.Expected{
  107. ExitCode: errdefs.ExitCodeLoginRequired,
  108. Err: `not logged in to azure, you need to run "docker login azure" first`,
  109. })
  110. })
  111. }
  112. func getTestLocation() string {
  113. rand.Seed(time.Now().Unix())
  114. n := rand.Intn(len(location))
  115. return location[n]
  116. }
  117. func uploadTestFile(t *testing.T, aciContext store.AciContext, accountName string, fileshareName string, testFileName string, testFileContent string) {
  118. storageLogin := login.StorageLoginImpl{AciContext: aciContext}
  119. key, err := storageLogin.GetAzureStorageAccountKey(context.TODO(), accountName)
  120. assert.NilError(t, err)
  121. cred, err := azfile.NewSharedKeyCredential(accountName, key)
  122. assert.NilError(t, err)
  123. u, _ := url.Parse(fmt.Sprintf("https://%s.file.core.windows.net/%s", accountName, fileshareName))
  124. uploadFile(t, *cred, u.String(), testFileName, testFileContent)
  125. }
  126. const (
  127. fileshareName = "dockertestshare"
  128. fileshareName2 = "dockertestshare2"
  129. )
  130. func TestRunVolume(t *testing.T) {
  131. const (
  132. testFileContent = "Volume mounted successfully!"
  133. testFileName = "index.html"
  134. )
  135. c := NewParallelE2eCLI(t, binDir)
  136. sID, rg, location := setupTestResourceGroup(t, c)
  137. // Bootstrap volume
  138. aciContext := store.AciContext{
  139. SubscriptionID: sID,
  140. Location: location,
  141. ResourceGroup: rg,
  142. }
  143. // Used in subtests
  144. var (
  145. container string
  146. hostIP string
  147. endpoint string
  148. volumeID string
  149. accountName = "e2e" + strconv.Itoa(int(time.Now().UnixNano()))
  150. )
  151. t.Run("check empty volume name validity", func(t *testing.T) {
  152. invalidName := ""
  153. res := c.RunDockerOrExitError("volume", "create", "--storage-account", invalidName, fileshareName)
  154. res.Assert(t, icmd.Expected{
  155. ExitCode: 1,
  156. Err: `parameter=accountName constraint=MinLength value="" details: value length must be greater than or equal to 3`,
  157. })
  158. })
  159. t.Run("check volume name validity", func(t *testing.T) {
  160. invalidName := "some-storage-123"
  161. res := c.RunDockerOrExitError("volume", "create", "--storage-account", invalidName, fileshareName)
  162. res.Assert(t, icmd.Expected{
  163. ExitCode: 1,
  164. Err: "some-storage-123 is not a valid storage account name. Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only.",
  165. })
  166. })
  167. t.Run("create volumes", func(t *testing.T) {
  168. c.RunDockerCmd("volume", "create", "--storage-account", accountName, fileshareName)
  169. })
  170. volumeID = accountName + "/" + fileshareName
  171. t.Cleanup(func() {
  172. c.RunDockerCmd("volume", "rm", volumeID)
  173. res := c.RunDockerCmd("volume", "ls")
  174. lines := lines(res.Stdout())
  175. assert.Equal(t, len(lines), 1)
  176. })
  177. t.Run("create second fileshare", func(t *testing.T) {
  178. c.RunDockerCmd("volume", "create", "--storage-account", accountName, fileshareName2)
  179. })
  180. volumeID2 := accountName + "/" + fileshareName2
  181. t.Run("list volumes", func(t *testing.T) {
  182. res := c.RunDockerCmd("volume", "ls", "--quiet")
  183. l := lines(res.Stdout())
  184. assert.Equal(t, len(l), 2)
  185. assert.Equal(t, l[0], volumeID)
  186. assert.Equal(t, l[1], volumeID2)
  187. res = c.RunDockerCmd("volume", "ls")
  188. l = lines(res.Stdout())
  189. assert.Equal(t, len(l), 3)
  190. firstAccount := l[1]
  191. fields := strings.Fields(firstAccount)
  192. assert.Equal(t, fields[0], volumeID)
  193. secondAccount := l[2]
  194. fields = strings.Fields(secondAccount)
  195. assert.Equal(t, fields[0], volumeID2)
  196. })
  197. t.Run("inspect volumes", func(t *testing.T) {
  198. res := c.RunDockerCmd("volume", "inspect", volumeID)
  199. assert.Equal(t, res.Stdout(), fmt.Sprintf(`{
  200. "ID": %q,
  201. "Description": "Fileshare %s in %s storage account"
  202. }
  203. `, volumeID, fileshareName, accountName))
  204. res = c.RunDockerCmd("volume", "inspect", volumeID2)
  205. assert.Equal(t, res.Stdout(), fmt.Sprintf(`{
  206. "ID": %q,
  207. "Description": "Fileshare %s in %s storage account"
  208. }
  209. `, volumeID2, fileshareName2, accountName))
  210. })
  211. t.Run("delete only fileshare", func(t *testing.T) {
  212. c.RunDockerCmd("volume", "rm", volumeID2)
  213. res := c.RunDockerCmd("volume", "ls")
  214. lines := lines(res.Stdout())
  215. assert.Equal(t, len(lines), 2)
  216. assert.Assert(t, !strings.Contains(res.Stdout(), fileshareName2), "second fileshare still visible after rm")
  217. })
  218. t.Run("upload file", func(t *testing.T) {
  219. uploadTestFile(t, aciContext, accountName, fileshareName, testFileName, testFileContent)
  220. })
  221. t.Run("run", func(t *testing.T) {
  222. mountTarget := "/usr/share/nginx/html"
  223. res := c.RunDockerCmd(
  224. "run", "-d",
  225. "-v", fmt.Sprintf("%s:%s", volumeID, mountTarget),
  226. "-p", "80:80",
  227. "nginx",
  228. )
  229. container = getContainerName(res.Stdout())
  230. })
  231. t.Run("inspect", func(t *testing.T) {
  232. res := c.RunDockerCmd("inspect", container)
  233. containerInspect, err := ParseContainerInspect(res.Stdout())
  234. assert.NilError(t, err)
  235. assert.Equal(t, containerInspect.Platform, "Linux")
  236. assert.Equal(t, containerInspect.HostConfig.CPULimit, 1.0)
  237. assert.Equal(t, containerInspect.HostConfig.CPUReservation, 1.0)
  238. assert.Equal(t, containerInspect.HostConfig.RestartPolicy, containers.RestartPolicyNone)
  239. assert.Assert(t, is.Len(containerInspect.Ports, 1))
  240. hostIP = containerInspect.Ports[0].HostIP
  241. endpoint = fmt.Sprintf("http://%s:%d", containerInspect.Ports[0].HostIP, containerInspect.Ports[0].HostPort)
  242. })
  243. t.Run("ps", func(t *testing.T) {
  244. res := c.RunDockerCmd("ps")
  245. out := lines(res.Stdout())
  246. l := out[len(out)-1]
  247. assert.Assert(t, strings.Contains(l, container), "Looking for %q in line: %s", container, l)
  248. assert.Assert(t, strings.Contains(l, "nginx"))
  249. assert.Assert(t, strings.Contains(l, "Running"))
  250. assert.Assert(t, strings.Contains(l, hostIP+":80->80/tcp"))
  251. })
  252. t.Run("http get", func(t *testing.T) {
  253. output := HTTPGetWithRetry(t, endpoint, http.StatusOK, 2*time.Second, 20*time.Second)
  254. assert.Assert(t, strings.Contains(output, testFileContent), "Actual content: "+output)
  255. })
  256. t.Run("logs", func(t *testing.T) {
  257. res := c.RunDockerCmd("logs", container)
  258. res.Assert(t, icmd.Expected{Out: "GET"})
  259. })
  260. t.Run("exec", func(t *testing.T) {
  261. res := c.RunDockerCmd("exec", container, "pwd")
  262. res.Assert(t, icmd.Expected{Out: "/"})
  263. res = c.RunDockerOrExitError("exec", container, "echo", "fail_with_argument")
  264. res.Assert(t, icmd.Expected{
  265. ExitCode: 1,
  266. Err: "ACI exec command does not accept arguments to the command. Only the binary should be specified",
  267. })
  268. })
  269. t.Run("logs follow", func(t *testing.T) {
  270. cmd := c.NewDockerCmd("logs", "--follow", container)
  271. res := icmd.StartCmd(cmd)
  272. checkUp := func(t poll.LogT) poll.Result {
  273. r, _ := http.Get(endpoint + "/is_up")
  274. if r != nil && r.StatusCode == http.StatusNotFound {
  275. return poll.Success()
  276. }
  277. return poll.Continue("waiting for container to serve request")
  278. }
  279. poll.WaitOn(t, checkUp, poll.WithDelay(1*time.Second), poll.WithTimeout(60*time.Second))
  280. assert.Assert(t, !strings.Contains(res.Stdout(), "/test"))
  281. checkLogs := func(t poll.LogT) poll.Result {
  282. if strings.Contains(res.Stdout(), "/test") {
  283. return poll.Success()
  284. }
  285. return poll.Continue("waiting for logs to contain /test")
  286. }
  287. // Do request on /test
  288. go func() {
  289. time.Sleep(3 * time.Second)
  290. _, _ = http.Get(endpoint + "/test")
  291. }()
  292. poll.WaitOn(t, checkLogs, poll.WithDelay(3*time.Second), poll.WithTimeout(20*time.Second))
  293. if runtime.GOOS == "windows" {
  294. err := res.Cmd.Process.Kill()
  295. assert.NilError(t, err)
  296. } else {
  297. err := res.Cmd.Process.Signal(syscall.SIGTERM)
  298. assert.NilError(t, err)
  299. }
  300. })
  301. t.Run("rm a running container", func(t *testing.T) {
  302. res := c.RunDockerOrExitError("rm", container)
  303. res.Assert(t, icmd.Expected{
  304. Err: fmt.Sprintf("Error: you cannot remove a running container %s. Stop the container before attempting removal or force remove", container),
  305. ExitCode: 1,
  306. })
  307. })
  308. t.Run("force rm", func(t *testing.T) {
  309. res := c.RunDockerCmd("rm", "-f", container)
  310. res.Assert(t, icmd.Expected{Out: container})
  311. checkStopped := func(t poll.LogT) poll.Result {
  312. res := c.RunDockerOrExitError("inspect", container)
  313. if res.ExitCode == 1 {
  314. return poll.Success()
  315. }
  316. return poll.Continue("waiting for container to stop")
  317. }
  318. poll.WaitOn(t, checkStopped, poll.WithDelay(5*time.Second), poll.WithTimeout(60*time.Second))
  319. })
  320. }
  321. func lines(output string) []string {
  322. return strings.Split(strings.TrimSpace(output), "\n")
  323. }
  324. func TestContainerRunAttached(t *testing.T) {
  325. c := NewParallelE2eCLI(t, binDir)
  326. _, groupID, location := setupTestResourceGroup(t, c)
  327. // Used in subtests
  328. var (
  329. container string = "test-container"
  330. endpoint string
  331. followLogsProcess *icmd.Result
  332. )
  333. t.Run("run attached limits", func(t *testing.T) {
  334. dnsLabelName := "nginx-" + groupID
  335. fqdn := dnsLabelName + "." + location + ".azurecontainer.io"
  336. cmd := c.NewDockerCmd(
  337. "run",
  338. "--name", container,
  339. "--restart", "on-failure",
  340. "--memory", "0.1G", "--cpus", "0.1",
  341. "-p", "80:80",
  342. "--domainname",
  343. dnsLabelName,
  344. "nginx",
  345. )
  346. followLogsProcess = icmd.StartCmd(cmd)
  347. checkRunning := func(t poll.LogT) poll.Result {
  348. res := c.RunDockerOrExitError("inspect", container)
  349. if res.ExitCode == 0 && strings.Contains(res.Stdout(), `"Status": "Running"`) && !strings.Contains(res.Stdout(), `"HostIP": ""`) {
  350. return poll.Success()
  351. }
  352. return poll.Continue("waiting for container to be running, current inspect result: \n%s", res.Combined())
  353. }
  354. poll.WaitOn(t, checkRunning, poll.WithDelay(5*time.Second), poll.WithTimeout(90*time.Second))
  355. inspectRes := c.RunDockerCmd("inspect", container)
  356. containerInspect, err := ParseContainerInspect(inspectRes.Stdout())
  357. assert.NilError(t, err)
  358. assert.Equal(t, containerInspect.Platform, "Linux")
  359. assert.Equal(t, containerInspect.HostConfig.CPULimit, 0.1)
  360. assert.Equal(t, containerInspect.HostConfig.MemoryLimit, uint64(107374182))
  361. assert.Equal(t, containerInspect.HostConfig.CPUReservation, 0.1)
  362. assert.Equal(t, containerInspect.HostConfig.MemoryReservation, uint64(107374182))
  363. assert.Equal(t, containerInspect.HostConfig.RestartPolicy, containers.RestartPolicyOnFailure)
  364. assert.Assert(t, is.Len(containerInspect.Ports, 1))
  365. port := containerInspect.Ports[0]
  366. assert.Assert(t, port.HostIP != "", "empty hostIP, inspect: \n"+inspectRes.Stdout())
  367. assert.Equal(t, port.ContainerPort, uint32(80))
  368. assert.Equal(t, port.HostPort, uint32(80))
  369. assert.Equal(t, containerInspect.Config.FQDN, fqdn)
  370. endpoint = fmt.Sprintf("http://%s:%d", fqdn, port.HostPort)
  371. assert.Assert(t, !strings.Contains(followLogsProcess.Stdout(), "/test"))
  372. checkRequest := func(t poll.LogT) poll.Result {
  373. r, _ := http.Get(endpoint + "/test")
  374. if r != nil && r.StatusCode == http.StatusNotFound {
  375. return poll.Success()
  376. }
  377. return poll.Continue("waiting for container to serve request")
  378. }
  379. poll.WaitOn(t, checkRequest, poll.WithDelay(1*time.Second), poll.WithTimeout(60*time.Second))
  380. checkLog := func(t poll.LogT) poll.Result {
  381. if strings.Contains(followLogsProcess.Stdout(), "/test") {
  382. return poll.Success()
  383. }
  384. return poll.Continue("waiting for logs to contain /test")
  385. }
  386. poll.WaitOn(t, checkLog, poll.WithDelay(1*time.Second), poll.WithTimeout(20*time.Second))
  387. })
  388. t.Run("stop wrong container", func(t *testing.T) {
  389. res := c.RunDockerOrExitError("stop", "unknown-container")
  390. res.Assert(t, icmd.Expected{
  391. Err: "Error: container unknown-container not found",
  392. ExitCode: 1,
  393. })
  394. })
  395. t.Run("stop container", func(t *testing.T) {
  396. res := c.RunDockerCmd("stop", container)
  397. res.Assert(t, icmd.Expected{Out: container})
  398. waitForStatus(t, c, container, "Terminated", "Node Stopped")
  399. })
  400. t.Run("check we stoppped following logs", func(t *testing.T) {
  401. // nolint errcheck
  402. followLogsStopped := waitWithTimeout(func() { followLogsProcess.Cmd.Process.Wait() }, 10*time.Second)
  403. assert.NilError(t, followLogsStopped, "Follow logs process did not stop after container is stopped")
  404. })
  405. t.Run("ps stopped container with --all", func(t *testing.T) {
  406. res := c.RunDockerCmd("ps", container)
  407. out := lines(res.Stdout())
  408. assert.Assert(t, is.Len(out, 1))
  409. res = c.RunDockerCmd("ps", "--all", container)
  410. out = lines(res.Stdout())
  411. assert.Assert(t, is.Len(out, 2))
  412. })
  413. t.Run("restart container", func(t *testing.T) {
  414. res := c.RunDockerCmd("start", container)
  415. res.Assert(t, icmd.Expected{Out: container})
  416. waitForStatus(t, c, container, convert.StatusRunning)
  417. })
  418. t.Run("prune dry run", func(t *testing.T) {
  419. res := c.RunDockerCmd("prune", "--dry-run")
  420. fmt.Println("prune output:")
  421. assert.Equal(t, "resources that would be deleted:\n", res.Stdout())
  422. res = c.RunDockerCmd("prune", "--dry-run", "--force")
  423. assert.Equal(t, "resources that would be deleted:\n"+container+"\n", res.Stdout())
  424. })
  425. t.Run("prune", func(t *testing.T) {
  426. res := c.RunDockerCmd("prune")
  427. assert.Equal(t, "deleted resources:\n", res.Stdout())
  428. res = c.RunDockerCmd("ps")
  429. l := lines(res.Stdout())
  430. assert.Equal(t, 2, len(l))
  431. res = c.RunDockerCmd("prune", "--force")
  432. assert.Equal(t, "deleted resources:\n"+container+"\n", res.Stdout())
  433. res = c.RunDockerCmd("ps", "--all")
  434. l = lines(res.Stdout())
  435. assert.Equal(t, 1, len(l))
  436. })
  437. }
  438. func overwriteFileStorageAccount(t *testing.T, absComposefileName string, storageAccount string) {
  439. data, err := ioutil.ReadFile(absComposefileName)
  440. assert.NilError(t, err)
  441. override := strings.Replace(string(data), "dockertestvolumeaccount", storageAccount, 1)
  442. err = ioutil.WriteFile(absComposefileName, []byte(override), 0644)
  443. assert.NilError(t, err)
  444. }
  445. func TestUpResources(t *testing.T) {
  446. const (
  447. composeProjectName = "testresources"
  448. serverContainer = composeProjectName + "_web"
  449. wordsContainer = composeProjectName + "_words"
  450. )
  451. c := NewParallelE2eCLI(t, binDir)
  452. setupTestResourceGroup(t, c)
  453. t.Run("compose up", func(t *testing.T) {
  454. c.RunDockerCmd("compose", "up", "-f", "../composefiles/aci-demo/aci_demo_port_resources.yaml", "--project-name", composeProjectName)
  455. res := c.RunDockerCmd("inspect", serverContainer)
  456. webInspect, err := ParseContainerInspect(res.Stdout())
  457. assert.NilError(t, err)
  458. assert.Equal(t, webInspect.HostConfig.CPULimit, 0.7)
  459. assert.Equal(t, webInspect.HostConfig.MemoryLimit, uint64(1073741824))
  460. assert.Equal(t, webInspect.HostConfig.CPUReservation, 0.5)
  461. assert.Equal(t, webInspect.HostConfig.MemoryReservation, uint64(536870912))
  462. res = c.RunDockerCmd("inspect", wordsContainer)
  463. wordsInspect, err := ParseContainerInspect(res.Stdout())
  464. assert.NilError(t, err)
  465. assert.Equal(t, wordsInspect.HostConfig.CPULimit, 0.5)
  466. assert.Equal(t, wordsInspect.HostConfig.MemoryLimit, uint64(751619276))
  467. assert.Equal(t, wordsInspect.HostConfig.CPUReservation, 0.5)
  468. assert.Equal(t, wordsInspect.HostConfig.MemoryReservation, uint64(751619276))
  469. })
  470. }
  471. func TestUpSecrets(t *testing.T) {
  472. const (
  473. composeProjectName = "aci_secrets"
  474. serverContainer = composeProjectName + "_web"
  475. secret1Name = "mytarget1"
  476. secret1Value = "myPassword1\n"
  477. secret2Name = "mysecret2"
  478. secret2Value = "another_password\n"
  479. )
  480. var (
  481. basefilePath = filepath.Join("..", "composefiles", composeProjectName)
  482. composefilePath = filepath.Join(basefilePath, "compose.yml")
  483. composefileInvalidTargetPath = filepath.Join(basefilePath, "compose-invalid-target.yml")
  484. )
  485. c := NewParallelE2eCLI(t, binDir)
  486. _, _, _ = setupTestResourceGroup(t, c)
  487. t.Run("compose up invalid target", func(t *testing.T) {
  488. res := c.RunDockerOrExitError("compose", "up", "-f", composefileInvalidTargetPath, "--project-name", composeProjectName)
  489. assert.Equal(t, res.ExitCode, 1)
  490. assert.Equal(t, res.Combined(), "in service \"web\", secret with source \"mysecret1\" cannot have a path as target. Found \"my/invalid/target1\"\n")
  491. })
  492. t.Run("compose up", func(t *testing.T) {
  493. c.RunDockerCmd("compose", "up", "-f", composefilePath, "--project-name", composeProjectName)
  494. res := c.RunDockerCmd("ps")
  495. out := lines(res.Stdout())
  496. // Check one container running
  497. assert.Assert(t, is.Len(out, 2))
  498. webRunning := false
  499. for _, l := range out {
  500. if strings.Contains(l, serverContainer) {
  501. webRunning = true
  502. strings.Contains(l, ":80->80/tcp")
  503. }
  504. }
  505. assert.Assert(t, webRunning, "web container not running ; ps:\n"+res.Stdout())
  506. res = c.RunDockerCmd("inspect", serverContainer)
  507. containerInspect, err := ParseContainerInspect(res.Stdout())
  508. assert.NilError(t, err)
  509. assert.Assert(t, is.Len(containerInspect.Ports, 1))
  510. endpoint := fmt.Sprintf("http://%s:%d", containerInspect.Ports[0].HostIP, containerInspect.Ports[0].HostPort)
  511. output := HTTPGetWithRetry(t, endpoint+"/"+secret1Name, http.StatusOK, 2*time.Second, 20*time.Second)
  512. // replace windows carriage return
  513. output = strings.ReplaceAll(output, "\r", "")
  514. assert.Equal(t, output, secret1Value)
  515. output = HTTPGetWithRetry(t, endpoint+"/"+secret2Name, http.StatusOK, 2*time.Second, 20*time.Second)
  516. output = strings.ReplaceAll(output, "\r", "")
  517. assert.Equal(t, output, secret2Value)
  518. t.Cleanup(func() {
  519. c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
  520. res := c.RunDockerCmd("ps")
  521. out := lines(res.Stdout())
  522. assert.Equal(t, len(out), 1)
  523. })
  524. })
  525. }
  526. func TestUpUpdate(t *testing.T) {
  527. const (
  528. composeProjectName = "acidemo"
  529. serverContainer = composeProjectName + "_web"
  530. wordsContainer = composeProjectName + "_words"
  531. dbContainer = composeProjectName + "_db"
  532. )
  533. var (
  534. singlePortVolumesComposefile = "aci_demo_port_volumes.yaml"
  535. multiPortComposefile = "aci_demo_multi_port.yaml"
  536. )
  537. c := NewParallelE2eCLI(t, binDir)
  538. sID, groupID, location := setupTestResourceGroup(t, c)
  539. composeAccountName := groupID + "-sa"
  540. composeAccountName = strings.ReplaceAll(composeAccountName, "-", "")
  541. composeAccountName = strings.ToLower(composeAccountName)
  542. dstDir := filepath.Join(os.TempDir(), "e2e-aci-volume-"+composeAccountName)
  543. srcDir := filepath.Join("..", "composefiles", "aci-demo")
  544. err := fileutil.CopyDirs(srcDir, dstDir)
  545. assert.NilError(t, err)
  546. t.Cleanup(func() {
  547. assert.NilError(t, os.RemoveAll(dstDir))
  548. })
  549. singlePortVolumesComposefile = filepath.Join(dstDir, singlePortVolumesComposefile)
  550. overwriteFileStorageAccount(t, singlePortVolumesComposefile, composeAccountName)
  551. multiPortComposefile = filepath.Join(dstDir, multiPortComposefile)
  552. volumeID := composeAccountName + "/" + fileshareName
  553. t.Run("compose up", func(t *testing.T) {
  554. const (
  555. testFileName = "msg.txt"
  556. testFileContent = "VOLUME_OK"
  557. projectName = "acidemo"
  558. )
  559. c.RunDockerCmd("volume", "create", "--storage-account", composeAccountName, fileshareName)
  560. // Bootstrap volume
  561. aciContext := store.AciContext{
  562. SubscriptionID: sID,
  563. Location: location,
  564. ResourceGroup: groupID,
  565. }
  566. uploadTestFile(t, aciContext, composeAccountName, fileshareName, testFileName, testFileContent)
  567. dnsLabelName := "nginx-" + groupID
  568. fqdn := dnsLabelName + "." + location + ".azurecontainer.io"
  569. // Name of Compose project is taken from current folder "acie2e"
  570. c.RunDockerCmd("compose", "up", "-f", singlePortVolumesComposefile, "--domainname", dnsLabelName, "--project-name", projectName)
  571. res := c.RunDockerCmd("ps")
  572. out := lines(res.Stdout())
  573. // Check three containers are running
  574. assert.Assert(t, is.Len(out, 4))
  575. webRunning := false
  576. for _, l := range out {
  577. if strings.Contains(l, serverContainer) {
  578. webRunning = true
  579. strings.Contains(l, ":80->80/tcp")
  580. }
  581. }
  582. assert.Assert(t, webRunning, "web container not running ; ps:\n"+res.Stdout())
  583. res = c.RunDockerCmd("inspect", serverContainer)
  584. containerInspect, err := ParseContainerInspect(res.Stdout())
  585. assert.NilError(t, err)
  586. assert.Assert(t, is.Len(containerInspect.Ports, 1))
  587. endpoint := fmt.Sprintf("http://%s:%d", containerInspect.Ports[0].HostIP, containerInspect.Ports[0].HostPort)
  588. output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
  589. assert.Assert(t, strings.Contains(output, `"word":`))
  590. endpoint = fmt.Sprintf("http://%s:%d", fqdn, containerInspect.Ports[0].HostPort)
  591. HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
  592. body := HTTPGetWithRetry(t, endpoint+"/volume_test/"+testFileName, http.StatusOK, 2*time.Second, 20*time.Second)
  593. assert.Assert(t, strings.Contains(body, testFileContent))
  594. // Try to remove the volume while it's still in use
  595. res = c.RunDockerOrExitError("volume", "rm", volumeID)
  596. res.Assert(t, icmd.Expected{
  597. ExitCode: 1,
  598. Err: fmt.Sprintf(`Error: volume "%s/%s" is used in container group %q`,
  599. composeAccountName, fileshareName, projectName),
  600. })
  601. })
  602. t.Cleanup(func() {
  603. c.RunDockerCmd("volume", "rm", volumeID)
  604. })
  605. t.Run("compose ps", func(t *testing.T) {
  606. res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName, "--quiet")
  607. l := lines(res.Stdout())
  608. assert.Assert(t, is.Len(l, 3))
  609. res = c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName)
  610. l = lines(res.Stdout())
  611. assert.Assert(t, is.Len(l, 4))
  612. var wordsDisplayed, webDisplayed, dbDisplayed bool
  613. for _, line := range l {
  614. fields := strings.Fields(line)
  615. containerID := fields[0]
  616. switch containerID {
  617. case wordsContainer:
  618. wordsDisplayed = true
  619. assert.DeepEqual(t, fields, []string{containerID, "words", "1/1"})
  620. case dbContainer:
  621. dbDisplayed = true
  622. assert.DeepEqual(t, fields, []string{containerID, "db", "1/1"})
  623. case serverContainer:
  624. webDisplayed = true
  625. assert.Equal(t, fields[1], "web")
  626. assert.Check(t, strings.Contains(fields[3], ":80->80/tcp"))
  627. }
  628. }
  629. assert.Check(t, webDisplayed && wordsDisplayed && dbDisplayed, "\n%s\n", res.Stdout())
  630. })
  631. t.Run("compose ls", func(t *testing.T) {
  632. res := c.RunDockerCmd("compose", "ls", "--quiet")
  633. l := lines(res.Stdout())
  634. assert.Assert(t, is.Len(l, 1))
  635. res = c.RunDockerCmd("compose", "ls")
  636. l = lines(res.Stdout())
  637. assert.Equal(t, 2, len(l))
  638. fields := strings.Fields(l[1])
  639. assert.Equal(t, 2, len(fields))
  640. assert.Equal(t, fields[0], composeProjectName)
  641. assert.Equal(t, "Running", fields[1])
  642. })
  643. t.Run("logs web", func(t *testing.T) {
  644. res := c.RunDockerCmd("logs", serverContainer)
  645. res.Assert(t, icmd.Expected{Out: "Listening on port 80"})
  646. })
  647. t.Run("update", func(t *testing.T) {
  648. c.RunDockerCmd("compose", "up", "-f", multiPortComposefile, "--project-name", composeProjectName)
  649. res := c.RunDockerCmd("ps")
  650. out := lines(res.Stdout())
  651. // Check three containers are running
  652. assert.Assert(t, is.Len(out, 4))
  653. for _, cName := range []string{serverContainer, wordsContainer} {
  654. res = c.RunDockerCmd("inspect", cName)
  655. containerInspect, err := ParseContainerInspect(res.Stdout())
  656. assert.NilError(t, err)
  657. assert.Assert(t, is.Len(containerInspect.Ports, 1))
  658. endpoint := fmt.Sprintf("http://%s:%d", containerInspect.Ports[0].HostIP, containerInspect.Ports[0].HostPort)
  659. var route string
  660. switch cName {
  661. case serverContainer:
  662. route = "/words/noun"
  663. assert.Equal(t, containerInspect.Ports[0].HostPort, uint32(80))
  664. assert.Equal(t, containerInspect.Ports[0].ContainerPort, uint32(80))
  665. case wordsContainer:
  666. route = "/noun"
  667. assert.Equal(t, containerInspect.Ports[0].HostPort, uint32(8080))
  668. assert.Equal(t, containerInspect.Ports[0].ContainerPort, uint32(8080))
  669. }
  670. HTTPGetWithRetry(t, endpoint+route, http.StatusOK, 1*time.Second, 60*time.Second)
  671. res = c.RunDockerCmd("ps")
  672. p := containerInspect.Ports[0]
  673. res.Assert(t, icmd.Expected{
  674. Out: fmt.Sprintf("%s:%d->%d/tcp", p.HostIP, p.HostPort, p.ContainerPort),
  675. })
  676. }
  677. })
  678. t.Run("down", func(t *testing.T) {
  679. c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
  680. res := c.RunDockerCmd("ps")
  681. out := lines(res.Stdout())
  682. assert.Equal(t, len(out), 1)
  683. })
  684. }
  685. /*
  686. func TestRunEnvVars(t *testing.T) {
  687. c := NewParallelE2eCLI(t, binDir)
  688. _, _, _ = setupTestResourceGroup(t, c)
  689. t.Run("run", func(t *testing.T) {
  690. cmd := c.NewDockerCmd(
  691. "run", "-d",
  692. "-e", "MYSQL_ROOT_PASSWORD=rootpwd",
  693. "-e", "MYSQL_DATABASE=mytestdb",
  694. "-e", "MYSQL_USER",
  695. "-e", "MYSQL_PASSWORD=userpwd",
  696. "-e", "DATASOURCE_URL=jdbc:mysql://mydb.mysql.database.azure.com/db1?useSSL=true&requireSSL=false&serverTimezone=America/Recife",
  697. "mysql:5.7",
  698. )
  699. cmd.Env = append(cmd.Env, "MYSQL_USER=user1")
  700. res := icmd.RunCmd(cmd)
  701. res.Assert(t, icmd.Success)
  702. out := lines(res.Stdout())
  703. container := strings.TrimSpace(out[len(out)-1])
  704. res = c.RunDockerCmd("inspect", container)
  705. containerInspect, err := ParseContainerInspect(res.Stdout())
  706. assert.NilError(t, err)
  707. assert.Assert(t, containerInspect.Config != nil, "nil container config")
  708. assert.Assert(t, containerInspect.Config.Env != nil, "nil container env variables")
  709. assert.Equal(t, containerInspect.Image, "mysql:5.7")
  710. envVars := containerInspect.Config.Env
  711. assert.Equal(t, len(envVars), 5)
  712. assert.Equal(t, envVars["MYSQL_ROOT_PASSWORD"], "rootpwd")
  713. assert.Equal(t, envVars["MYSQL_DATABASE"], "mytestdb")
  714. assert.Equal(t, envVars["MYSQL_USER"], "user1")
  715. assert.Equal(t, envVars["MYSQL_PASSWORD"], "userpwd")
  716. assert.Equal(t, envVars["DATASOURCE_URL"], "jdbc:mysql://mydb.mysql.database.azure.com/db1?useSSL=true&requireSSL=false&serverTimezone=America/Recife")
  717. check := func(t poll.LogT) poll.Result {
  718. res := c.RunDockerOrExitError("logs", container)
  719. if strings.Contains(res.Stdout(), "Giving user user1 access to schema mytestdb") {
  720. return poll.Success()
  721. }
  722. return poll.Continue("waiting for DB container to be up\n%s", res.Combined())
  723. }
  724. poll.WaitOn(t, check, poll.WithDelay(5*time.Second), poll.WithTimeout(90*time.Second))
  725. })
  726. }
  727. */
  728. func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string, string) {
  729. startTime := strconv.Itoa(int(time.Now().Unix()))
  730. rg := "E2E-" + t.Name() + "-" + startTime[5:]
  731. azureLogin(t, c)
  732. sID := getSubscriptionID(t)
  733. location := getTestLocation()
  734. err := createResourceGroup(t, sID, rg, location)
  735. assert.Check(t, is.Nil(err))
  736. t.Cleanup(func() {
  737. if err := deleteResourceGroup(t, rg); err != nil {
  738. t.Error(err)
  739. }
  740. })
  741. createAciContextAndUseIt(t, c, sID, rg, location)
  742. // Check nothing is running
  743. res := c.RunDockerCmd("ps")
  744. assert.Assert(t, is.Len(lines(res.Stdout()), 1))
  745. return sID, rg, location
  746. }
  747. func deleteResourceGroup(t *testing.T, rgName string) error {
  748. fmt.Printf(" [%s] deleting resource group %s\n", t.Name(), rgName)
  749. ctx := context.TODO()
  750. helper := aci.NewACIResourceGroupHelper()
  751. models, err := helper.GetSubscriptionIDs(ctx)
  752. if err != nil {
  753. return err
  754. }
  755. if len(models) == 0 {
  756. return errors.New("unable to delete resource group: no models")
  757. }
  758. return helper.DeleteAsync(ctx, *models[0].SubscriptionID, rgName)
  759. }
  760. func azureLogin(t *testing.T, c *E2eCLI) {
  761. // in order to create new service principal and get these 3 values : `az ad sp create-for-rbac --name 'TestServicePrincipal' --sdk-auth`
  762. clientID := os.Getenv("AZURE_CLIENT_ID")
  763. clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
  764. tenantID := os.Getenv("AZURE_TENANT_ID")
  765. assert.Check(t, clientID != "", "AZURE_CLIENT_ID must not be empty")
  766. assert.Check(t, clientSecret != "", "AZURE_CLIENT_SECRET must not be empty")
  767. assert.Check(t, tenantID != "", "AZURE_TENANT_ID must not be empty")
  768. c.RunDockerCmd("login", "azure", "--client-id", clientID, "--client-secret", clientSecret, "--tenant-id", tenantID)
  769. }
  770. func getSubscriptionID(t *testing.T) string {
  771. ctx := context.TODO()
  772. helper := aci.NewACIResourceGroupHelper()
  773. models, err := helper.GetSubscriptionIDs(ctx)
  774. assert.Check(t, is.Nil(err))
  775. assert.Check(t, len(models) == 1)
  776. return *models[0].SubscriptionID
  777. }
  778. func createResourceGroup(t *testing.T, sID, rgName string, location string) error {
  779. fmt.Printf(" [%s] creating resource group %s\n", t.Name(), rgName)
  780. helper := aci.NewACIResourceGroupHelper()
  781. _, err := helper.CreateOrUpdate(context.TODO(), sID, rgName, resources.Group{Location: to.StringPtr(location)})
  782. return err
  783. }
  784. func createAciContextAndUseIt(t *testing.T, c *E2eCLI, sID, rgName string, location string) {
  785. res := c.RunDockerCmd("context", "create", "aci", contextName, "--subscription-id", sID, "--resource-group", rgName, "--location", location)
  786. res.Assert(t, icmd.Expected{Out: "Successfully created aci context \"" + contextName + "\""})
  787. res = c.RunDockerCmd("context", "use", contextName)
  788. res.Assert(t, icmd.Expected{Out: contextName})
  789. res = c.RunDockerCmd("context", "ls")
  790. res.Assert(t, icmd.Expected{Out: contextName + " *"})
  791. }
  792. func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName, content string) {
  793. fURL, err := url.Parse(baseURL + "/" + fileName)
  794. assert.NilError(t, err)
  795. fileURL := azfile.NewFileURL(*fURL, azfile.NewPipeline(&cred, azfile.PipelineOptions{}))
  796. err = azfile.UploadBufferToAzureFile(context.TODO(), []byte(content), fileURL, azfile.UploadToAzureFileOptions{})
  797. assert.NilError(t, err)
  798. }
  799. func getContainerName(stdout string) string {
  800. out := lines(stdout)
  801. return strings.TrimSpace(out[len(out)-1])
  802. }
  803. func waitForStatus(t *testing.T, c *E2eCLI, containerID string, statuses ...string) {
  804. checkStopped := func(logt poll.LogT) poll.Result {
  805. res := c.RunDockerCmd("inspect", containerID)
  806. containerInspect, err := ParseContainerInspect(res.Stdout())
  807. assert.NilError(t, err)
  808. for _, status := range statuses {
  809. if containerInspect.Status == status {
  810. return poll.Success()
  811. }
  812. }
  813. return poll.Continue("Status %s != %s (expected) for container %s", containerInspect.Status, statuses, containerID)
  814. }
  815. poll.WaitOn(t, checkStopped, poll.WithDelay(5*time.Second), poll.WithTimeout(90*time.Second))
  816. }
  817. func waitWithTimeout(blockingCall func(), timeout time.Duration) error {
  818. c := make(chan struct{})
  819. go func() {
  820. defer close(c)
  821. blockingCall()
  822. }()
  823. select {
  824. case <-c:
  825. return nil
  826. case <-time.After(timeout):
  827. return fmt.Errorf("Timed out after %s", timeout)
  828. }
  829. }