backend.go 10.0 KB

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