backend.go 9.4 KB

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