backend.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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. "net/http"
  18. "strconv"
  19. "strings"
  20. "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
  21. "github.com/Azure/go-autorest/autorest/to"
  22. "github.com/compose-spec/compose-go/cli"
  23. "github.com/compose-spec/compose-go/types"
  24. "github.com/pkg/errors"
  25. "github.com/sirupsen/logrus"
  26. "github.com/docker/api/aci/convert"
  27. "github.com/docker/api/aci/login"
  28. "github.com/docker/api/backend"
  29. "github.com/docker/api/compose"
  30. "github.com/docker/api/containers"
  31. apicontext "github.com/docker/api/context"
  32. "github.com/docker/api/context/cloud"
  33. "github.com/docker/api/context/store"
  34. "github.com/docker/api/errdefs"
  35. ecstypes "github.com/docker/ecs-plugin/pkg/compose"
  36. )
  37. const (
  38. backendType = store.AciContextType
  39. singleContainerTag = "docker-single-container"
  40. composeContainerTag = "docker-compose-application"
  41. composeContainerSeparator = "_"
  42. statusUnknown = "Unknown"
  43. )
  44. // ErrNoSuchContainer is returned when the mentioned container does not exist
  45. var ErrNoSuchContainer = errors.New("no such container")
  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. }
  57. func init() {
  58. backend.Register(backendType, backendType, service, getCloudService)
  59. }
  60. func service(ctx context.Context) (backend.Service, error) {
  61. contextStore := store.ContextStore(ctx)
  62. currentContext := apicontext.CurrentContext(ctx)
  63. var aciContext store.AciContext
  64. if err := contextStore.GetEndpoint(currentContext, &aciContext); err != nil {
  65. return nil, err
  66. }
  67. return getAciAPIService(aciContext), nil
  68. }
  69. func getCloudService() (cloud.Service, error) {
  70. service, err := login.NewAzureLoginService()
  71. if err != nil {
  72. return nil, err
  73. }
  74. return &aciCloudService{
  75. loginService: service,
  76. }, nil
  77. }
  78. func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
  79. return &aciAPIService{
  80. aciContainerService: &aciContainerService{
  81. ctx: aciCtx,
  82. },
  83. aciComposeService: &aciComposeService{
  84. ctx: aciCtx,
  85. },
  86. }
  87. }
  88. type aciAPIService struct {
  89. *aciContainerService
  90. *aciComposeService
  91. }
  92. func (a *aciAPIService) ContainerService() containers.Service {
  93. return a.aciContainerService
  94. }
  95. func (a *aciAPIService) ComposeService() compose.Service {
  96. return a.aciComposeService
  97. }
  98. type aciContainerService struct {
  99. ctx store.AciContext
  100. }
  101. func (cs *aciContainerService) List(ctx context.Context, _ bool) ([]containers.Container, error) {
  102. groupsClient, err := getContainerGroupsClient(cs.ctx.SubscriptionID)
  103. if err != nil {
  104. return nil, err
  105. }
  106. var containerGroups []containerinstance.ContainerGroup
  107. result, err := groupsClient.ListByResourceGroup(ctx, cs.ctx.ResourceGroup)
  108. if err != nil {
  109. return []containers.Container{}, err
  110. }
  111. for result.NotDone() {
  112. containerGroups = append(containerGroups, result.Values()...)
  113. if err := result.NextWithContext(ctx); err != nil {
  114. return []containers.Container{}, err
  115. }
  116. }
  117. var res []containers.Container
  118. for _, containerGroup := range containerGroups {
  119. group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, *containerGroup.Name)
  120. if err != nil {
  121. return []containers.Container{}, err
  122. }
  123. if _, ok := group.Tags[singleContainerTag]; ok {
  124. if group.Containers == nil || len(*group.Containers) < 1 {
  125. return []containers.Container{}, fmt.Errorf("no containers found in ACI container group %s", *containerGroup.Name)
  126. }
  127. container := (*group.Containers)[0]
  128. c := getContainer(*containerGroup.Name, group.IPAddress, container)
  129. res = append(res, c)
  130. continue
  131. }
  132. for _, container := range *group.Containers {
  133. var containerID string
  134. // don't list sidecar container
  135. if *container.Name == convert.ComposeDNSSidecarName {
  136. continue
  137. }
  138. containerID = *containerGroup.Name + composeContainerSeparator + *container.Name
  139. c := getContainer(containerID, group.IPAddress, container)
  140. res = append(res, c)
  141. }
  142. }
  143. return res, nil
  144. }
  145. func getContainer(containerID string, ipAddress *containerinstance.IPAddress, container containerinstance.Container) containers.Container {
  146. status := statusUnknown
  147. if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
  148. status = *container.InstanceView.CurrentState.State
  149. }
  150. return containers.Container{
  151. ID: containerID,
  152. Image: *container.Image,
  153. Status: status,
  154. Ports: convert.ToPorts(ipAddress, *container.Ports),
  155. }
  156. }
  157. func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
  158. if strings.Contains(r.ID, composeContainerSeparator) {
  159. return errors.New(fmt.Sprintf("invalid container name. ACI container name cannot include %q", composeContainerSeparator))
  160. }
  161. project, err := convert.ContainerToComposeProject(r)
  162. if err != nil {
  163. return err
  164. }
  165. logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID)
  166. groupDefinition, err := convert.ToContainerGroup(cs.ctx, project)
  167. if err != nil {
  168. return err
  169. }
  170. addTag(&groupDefinition, singleContainerTag)
  171. return createACIContainers(ctx, cs.ctx, groupDefinition)
  172. }
  173. func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
  174. if groupDefinition.Tags == nil {
  175. groupDefinition.Tags = make(map[string]*string, 1)
  176. }
  177. groupDefinition.Tags[tagName] = to.StringPtr(tagName)
  178. }
  179. func (cs *aciContainerService) Stop(ctx context.Context, containerName string, timeout *uint32) error {
  180. return errdefs.ErrNotImplemented
  181. }
  182. func getGroupAndContainerName(containerID string) (string, string) {
  183. tokens := strings.Split(containerID, composeContainerSeparator)
  184. groupName := tokens[0]
  185. containerName := groupName
  186. if len(tokens) > 1 {
  187. containerName = tokens[len(tokens)-1]
  188. groupName = containerID[:len(containerID)-(len(containerName)+1)]
  189. }
  190. return groupName, containerName
  191. }
  192. func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
  193. err := verifyExecCommand(request.Command)
  194. if err != nil {
  195. return err
  196. }
  197. groupName, containerAciName := getGroupAndContainerName(name)
  198. containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
  199. if err != nil {
  200. return err
  201. }
  202. return exec(
  203. context.Background(),
  204. *containerExecResponse.WebSocketURI,
  205. *containerExecResponse.Password,
  206. request,
  207. )
  208. }
  209. func verifyExecCommand(command string) error {
  210. tokens := strings.Split(command, " ")
  211. if len(tokens) > 1 {
  212. return errors.New("ACI exec command does not accept arguments to the command. " +
  213. "Only the binary should be specified")
  214. }
  215. return nil
  216. }
  217. func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
  218. groupName, containerAciName := getGroupAndContainerName(containerName)
  219. var tail *int32
  220. if req.Follow {
  221. return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
  222. }
  223. if req.Tail != "all" {
  224. reqTail, err := strconv.Atoi(req.Tail)
  225. if err != nil {
  226. return err
  227. }
  228. i32 := int32(reqTail)
  229. tail = &i32
  230. }
  231. logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
  232. if err != nil {
  233. return err
  234. }
  235. _, err = fmt.Fprint(req.Writer, logs)
  236. return err
  237. }
  238. func (cs *aciContainerService) Delete(ctx context.Context, containerID string, _ bool) error {
  239. groupName, containerName := getGroupAndContainerName(containerID)
  240. if groupName != containerID {
  241. msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
  242. return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
  243. }
  244. cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
  245. if err != nil {
  246. return err
  247. }
  248. if cg.StatusCode == http.StatusNoContent {
  249. return ErrNoSuchContainer
  250. }
  251. return err
  252. }
  253. func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
  254. groupName, containerName := getGroupAndContainerName(containerID)
  255. cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
  256. if err != nil {
  257. return containers.Container{}, err
  258. }
  259. if cg.StatusCode == http.StatusNoContent {
  260. return containers.Container{}, ErrNoSuchContainer
  261. }
  262. var cc containerinstance.Container
  263. var found = false
  264. for _, c := range *cg.Containers {
  265. if to.String(c.Name) == containerName {
  266. cc = c
  267. found = true
  268. break
  269. }
  270. }
  271. if !found {
  272. return containers.Container{}, ErrNoSuchContainer
  273. }
  274. return convert.ContainerGroupToContainer(containerID, cg, cc)
  275. }
  276. type aciComposeService struct {
  277. ctx store.AciContext
  278. }
  279. func (cs *aciComposeService) Up(ctx context.Context, opts cli.ProjectOptions) error {
  280. project, err := cli.ProjectFromOptions(&opts)
  281. if err != nil {
  282. return err
  283. }
  284. logrus.Debugf("Up on project with name %q\n", project.Name)
  285. groupDefinition, err := convert.ToContainerGroup(cs.ctx, *project)
  286. addTag(&groupDefinition, composeContainerTag)
  287. if err != nil {
  288. return err
  289. }
  290. return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
  291. }
  292. func (cs *aciComposeService) Down(ctx context.Context, opts cli.ProjectOptions) error {
  293. var project types.Project
  294. if opts.Name != "" {
  295. project = types.Project{Name: opts.Name}
  296. } else {
  297. fullProject, err := cli.ProjectFromOptions(&opts)
  298. if err != nil {
  299. return err
  300. }
  301. project = *fullProject
  302. }
  303. logrus.Debugf("Down on project with name %q\n", project.Name)
  304. cg, err := deleteACIContainerGroup(ctx, cs.ctx, project.Name)
  305. if err != nil {
  306. return err
  307. }
  308. if cg.StatusCode == http.StatusNoContent {
  309. return ErrNoSuchContainer
  310. }
  311. return err
  312. }
  313. func (cs *aciComposeService) Ps(ctx context.Context, opts cli.ProjectOptions) ([]ecstypes.ServiceStatus, error) {
  314. return nil, errdefs.ErrNotImplemented
  315. }
  316. func (cs *aciComposeService) Logs(ctx context.Context, opts cli.ProjectOptions) error {
  317. return errdefs.ErrNotImplemented
  318. }
  319. type aciCloudService struct {
  320. loginService login.AzureLoginService
  321. }
  322. func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
  323. createOpts := params.(LoginParams)
  324. return cs.loginService.Login(ctx, createOpts.TenantID)
  325. }
  326. func (cs *aciCloudService) Logout(ctx context.Context) error {
  327. return cs.loginService.Logout(ctx)
  328. }
  329. func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
  330. contextHelper := newContextCreateHelper()
  331. createOpts := params.(ContextParams)
  332. return contextHelper.createContextData(ctx, createOpts)
  333. }