backend.go 10 KB

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