backend.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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 aci
  14. import (
  15. "context"
  16. "fmt"
  17. "io"
  18. "net/http"
  19. "strconv"
  20. "strings"
  21. "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
  22. "github.com/Azure/go-autorest/autorest"
  23. "github.com/Azure/go-autorest/autorest/to"
  24. "github.com/compose-spec/compose-go/cli"
  25. "github.com/compose-spec/compose-go/types"
  26. "github.com/pkg/errors"
  27. "github.com/sirupsen/logrus"
  28. "github.com/docker/compose-cli/aci/convert"
  29. "github.com/docker/compose-cli/aci/login"
  30. "github.com/docker/compose-cli/backend"
  31. "github.com/docker/compose-cli/compose"
  32. "github.com/docker/compose-cli/containers"
  33. apicontext "github.com/docker/compose-cli/context"
  34. "github.com/docker/compose-cli/context/cloud"
  35. "github.com/docker/compose-cli/context/store"
  36. "github.com/docker/compose-cli/errdefs"
  37. "github.com/docker/compose-cli/secrets"
  38. )
  39. const (
  40. backendType = store.AciContextType
  41. singleContainerTag = "docker-single-container"
  42. composeContainerTag = "docker-compose-application"
  43. composeContainerSeparator = "_"
  44. statusRunning = "Running"
  45. )
  46. // ContextParams options for creating ACI context
  47. type ContextParams struct {
  48. Description string
  49. Location string
  50. SubscriptionID string
  51. ResourceGroup string
  52. }
  53. // LoginParams azure login options
  54. type LoginParams struct {
  55. TenantID string
  56. ClientID string
  57. ClientSecret string
  58. }
  59. // Validate returns an error if options are not used properly
  60. func (opts LoginParams) Validate() error {
  61. if opts.ClientID != "" || opts.ClientSecret != "" {
  62. if opts.ClientID == "" || opts.ClientSecret == "" || opts.TenantID == "" {
  63. return errors.New("for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
  64. }
  65. }
  66. return nil
  67. }
  68. func init() {
  69. backend.Register(backendType, backendType, service, getCloudService)
  70. }
  71. func service(ctx context.Context) (backend.Service, error) {
  72. contextStore := store.ContextStore(ctx)
  73. currentContext := apicontext.CurrentContext(ctx)
  74. var aciContext store.AciContext
  75. if err := contextStore.GetEndpoint(currentContext, &aciContext); err != nil {
  76. return nil, err
  77. }
  78. return getAciAPIService(aciContext), nil
  79. }
  80. func getCloudService() (cloud.Service, error) {
  81. service, err := login.NewAzureLoginService()
  82. if err != nil {
  83. return nil, err
  84. }
  85. return &aciCloudService{
  86. loginService: service,
  87. }, nil
  88. }
  89. func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
  90. return &aciAPIService{
  91. aciContainerService: &aciContainerService{
  92. ctx: aciCtx,
  93. },
  94. aciComposeService: &aciComposeService{
  95. ctx: aciCtx,
  96. },
  97. }
  98. }
  99. type aciAPIService struct {
  100. *aciContainerService
  101. *aciComposeService
  102. }
  103. func (a *aciAPIService) ContainerService() containers.Service {
  104. return a.aciContainerService
  105. }
  106. func (a *aciAPIService) ComposeService() compose.Service {
  107. return a.aciComposeService
  108. }
  109. func (a *aciAPIService) SecretsService() secrets.Service {
  110. return nil
  111. }
  112. type aciContainerService struct {
  113. ctx store.AciContext
  114. }
  115. func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) {
  116. groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
  117. if err != nil {
  118. return nil, err
  119. }
  120. var containerGroups []containerinstance.ContainerGroup
  121. result, err := groupsClient.ListByResourceGroup(ctx, cs.ctx.ResourceGroup)
  122. if err != nil {
  123. return []containers.Container{}, err
  124. }
  125. for result.NotDone() {
  126. containerGroups = append(containerGroups, result.Values()...)
  127. if err := result.NextWithContext(ctx); err != nil {
  128. return []containers.Container{}, err
  129. }
  130. }
  131. var res []containers.Container
  132. for _, containerGroup := range containerGroups {
  133. group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, *containerGroup.Name)
  134. if err != nil {
  135. return []containers.Container{}, err
  136. }
  137. if group.Containers == nil || len(*group.Containers) < 1 {
  138. return []containers.Container{}, fmt.Errorf("no containers found in ACI container group %s", *containerGroup.Name)
  139. }
  140. for _, container := range *group.Containers {
  141. // don't list sidecar container
  142. if *container.Name == convert.ComposeDNSSidecarName {
  143. continue
  144. }
  145. if !all && convert.GetStatus(container, group) != statusRunning {
  146. continue
  147. }
  148. containerID := *containerGroup.Name + composeContainerSeparator + *container.Name
  149. if _, ok := group.Tags[singleContainerTag]; ok {
  150. containerID = *containerGroup.Name
  151. }
  152. c := convert.ContainerGroupToContainer(containerID, group, container)
  153. res = append(res, c)
  154. }
  155. }
  156. return res, nil
  157. }
  158. func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
  159. if strings.Contains(r.ID, composeContainerSeparator) {
  160. return errors.New(fmt.Sprintf("invalid container name. ACI container name cannot include %q", composeContainerSeparator))
  161. }
  162. project, err := convert.ContainerToComposeProject(r)
  163. if err != nil {
  164. return err
  165. }
  166. logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID)
  167. groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project)
  168. if err != nil {
  169. return err
  170. }
  171. addTag(&groupDefinition, singleContainerTag)
  172. return createACIContainers(ctx, cs.ctx, groupDefinition)
  173. }
  174. func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
  175. if groupDefinition.Tags == nil {
  176. groupDefinition.Tags = make(map[string]*string, 1)
  177. }
  178. groupDefinition.Tags[tagName] = to.StringPtr(tagName)
  179. }
  180. func (cs *aciContainerService) Start(ctx context.Context, containerID string) error {
  181. groupName, containerName := getGroupAndContainerName(containerID)
  182. if groupName != containerID {
  183. msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s"
  184. return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
  185. }
  186. containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
  187. if err != nil {
  188. return err
  189. }
  190. future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName)
  191. if err != nil {
  192. var aerr autorest.DetailedError
  193. if ok := errors.As(err, &aerr); ok {
  194. if aerr.StatusCode == http.StatusNotFound {
  195. return errdefs.ErrNotFound
  196. }
  197. }
  198. return err
  199. }
  200. return future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
  201. }
  202. func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error {
  203. if timeout != nil && *timeout != uint32(0) {
  204. return errors.Errorf("ACI integration does not support setting a timeout to stop a container before killing it.")
  205. }
  206. groupName, containerName := getGroupAndContainerName(containerID)
  207. if groupName != containerID {
  208. msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s"
  209. return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
  210. }
  211. return stopACIContainerGroup(ctx, cs.ctx, groupName)
  212. }
  213. func getGroupAndContainerName(containerID string) (string, string) {
  214. tokens := strings.Split(containerID, composeContainerSeparator)
  215. groupName := tokens[0]
  216. containerName := groupName
  217. if len(tokens) > 1 {
  218. containerName = tokens[len(tokens)-1]
  219. groupName = containerID[:len(containerID)-(len(containerName)+1)]
  220. }
  221. return groupName, containerName
  222. }
  223. func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
  224. err := verifyExecCommand(request.Command)
  225. if err != nil {
  226. return err
  227. }
  228. groupName, containerAciName := getGroupAndContainerName(name)
  229. containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
  230. if err != nil {
  231. return err
  232. }
  233. return exec(
  234. context.Background(),
  235. *containerExecResponse.WebSocketURI,
  236. *containerExecResponse.Password,
  237. request,
  238. )
  239. }
  240. func verifyExecCommand(command string) error {
  241. tokens := strings.Split(command, " ")
  242. if len(tokens) > 1 {
  243. return errors.New("ACI exec command does not accept arguments to the command. " +
  244. "Only the binary should be specified")
  245. }
  246. return nil
  247. }
  248. func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
  249. groupName, containerAciName := getGroupAndContainerName(containerName)
  250. var tail *int32
  251. if req.Follow {
  252. return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
  253. }
  254. if req.Tail != "all" {
  255. reqTail, err := strconv.Atoi(req.Tail)
  256. if err != nil {
  257. return err
  258. }
  259. i32 := int32(reqTail)
  260. tail = &i32
  261. }
  262. logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
  263. if err != nil {
  264. return err
  265. }
  266. _, err = fmt.Fprint(req.Writer, logs)
  267. return err
  268. }
  269. func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error {
  270. groupName, containerName := getGroupAndContainerName(containerID)
  271. if groupName != containerID {
  272. msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
  273. return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
  274. }
  275. if !request.Force {
  276. containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
  277. if err != nil {
  278. return err
  279. }
  280. cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName)
  281. if err != nil {
  282. if cg.StatusCode == http.StatusNotFound {
  283. return errdefs.ErrNotFound
  284. }
  285. return err
  286. }
  287. for _, container := range *cg.Containers {
  288. status := convert.GetStatus(container, cg)
  289. if status == statusRunning {
  290. return errdefs.ErrForbidden
  291. }
  292. }
  293. }
  294. cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
  295. // Delete returns `StatusNoContent` if the group is not found
  296. if cg.StatusCode == http.StatusNoContent {
  297. return errdefs.ErrNotFound
  298. }
  299. if err != nil {
  300. return err
  301. }
  302. return err
  303. }
  304. func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
  305. groupName, containerName := getGroupAndContainerName(containerID)
  306. cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
  307. if err != nil {
  308. return containers.Container{}, err
  309. }
  310. if cg.StatusCode == http.StatusNoContent {
  311. return containers.Container{}, errdefs.ErrNotFound
  312. }
  313. var cc containerinstance.Container
  314. var found = false
  315. for _, c := range *cg.Containers {
  316. if to.String(c.Name) == containerName {
  317. cc = c
  318. found = true
  319. break
  320. }
  321. }
  322. if !found {
  323. return containers.Container{}, errdefs.ErrNotFound
  324. }
  325. return convert.ContainerGroupToContainer(containerID, cg, cc), nil
  326. }
  327. type aciComposeService struct {
  328. ctx store.AciContext
  329. }
  330. func (cs *aciComposeService) Up(ctx context.Context, opts *cli.ProjectOptions) error {
  331. project, err := cli.ProjectFromOptions(opts)
  332. if err != nil {
  333. return err
  334. }
  335. logrus.Debugf("Up on project with name %q\n", project.Name)
  336. groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project)
  337. addTag(&groupDefinition, composeContainerTag)
  338. if err != nil {
  339. return err
  340. }
  341. return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
  342. }
  343. func (cs *aciComposeService) Down(ctx context.Context, opts *cli.ProjectOptions) error {
  344. var project types.Project
  345. if opts.Name != "" {
  346. project = types.Project{Name: opts.Name}
  347. } else {
  348. fullProject, err := cli.ProjectFromOptions(opts)
  349. if err != nil {
  350. return err
  351. }
  352. project = *fullProject
  353. }
  354. logrus.Debugf("Down on project with name %q\n", project.Name)
  355. cg, err := deleteACIContainerGroup(ctx, cs.ctx, project.Name)
  356. if err != nil {
  357. return err
  358. }
  359. if cg.StatusCode == http.StatusNoContent {
  360. return errdefs.ErrNotFound
  361. }
  362. return err
  363. }
  364. func (cs *aciComposeService) Ps(ctx context.Context, opts *cli.ProjectOptions) ([]compose.ServiceStatus, error) {
  365. return nil, errdefs.ErrNotImplemented
  366. }
  367. func (cs *aciComposeService) Logs(ctx context.Context, opts *cli.ProjectOptions, w io.Writer) error {
  368. return errdefs.ErrNotImplemented
  369. }
  370. func (cs *aciComposeService) Convert(ctx context.Context, opts *cli.ProjectOptions) ([]byte, error) {
  371. return nil, errdefs.ErrNotImplemented
  372. }
  373. type aciCloudService struct {
  374. loginService login.AzureLoginServiceAPI
  375. }
  376. func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
  377. opts, ok := params.(LoginParams)
  378. if !ok {
  379. return errors.New("Could not read azure LoginParams struct from generic parameter")
  380. }
  381. if opts.ClientID != "" {
  382. return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID)
  383. }
  384. return cs.loginService.Login(ctx, opts.TenantID)
  385. }
  386. func (cs *aciCloudService) Logout(ctx context.Context) error {
  387. return cs.loginService.Logout(ctx)
  388. }
  389. func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
  390. contextHelper := newContextCreateHelper()
  391. createOpts := params.(ContextParams)
  392. return contextHelper.createContextData(ctx, createOpts)
  393. }