backend.go 10 KB

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