backend.go 11 KB

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