backend.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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. "io"
  18. "net/http"
  19. "strconv"
  20. "strings"
  21. "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
  22. "github.com/Azure/go-autorest/autorest/to"
  23. "github.com/compose-spec/compose-go/cli"
  24. "github.com/compose-spec/compose-go/types"
  25. "github.com/pkg/errors"
  26. "github.com/sirupsen/logrus"
  27. "github.com/docker/api/azure/convert"
  28. "github.com/docker/api/azure/login"
  29. "github.com/docker/api/backend"
  30. "github.com/docker/api/compose"
  31. "github.com/docker/api/containers"
  32. apicontext "github.com/docker/api/context"
  33. "github.com/docker/api/context/cloud"
  34. "github.com/docker/api/context/store"
  35. "github.com/docker/api/errdefs"
  36. )
  37. const (
  38. singleContainerTag = "single--container--aci"
  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("no containers to run")
  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. if groupDefinition.Tags == nil {
  158. groupDefinition.Tags = make(map[string]*string, 1)
  159. }
  160. groupDefinition.Tags[singleContainerTag] = to.StringPtr("")
  161. return createACIContainers(ctx, cs.ctx, groupDefinition)
  162. }
  163. func (cs *aciContainerService) Stop(ctx context.Context, containerName string, timeout *uint32) error {
  164. return errdefs.ErrNotImplemented
  165. }
  166. func getGroupAndContainerName(containerID string) (string, string) {
  167. tokens := strings.Split(containerID, composeContainerSeparator)
  168. groupName := tokens[0]
  169. containerName := groupName
  170. if len(tokens) > 1 {
  171. containerName = tokens[len(tokens)-1]
  172. groupName = containerID[:len(containerID)-(len(containerName)+1)]
  173. }
  174. return groupName, containerName
  175. }
  176. func (cs *aciContainerService) Exec(ctx context.Context, name string, command string, reader io.Reader, writer io.Writer) error {
  177. err := verifyExecCommand(command)
  178. if err != nil {
  179. return err
  180. }
  181. groupName, containerAciName := getGroupAndContainerName(name)
  182. containerExecResponse, err := execACIContainer(ctx, cs.ctx, command, groupName, containerAciName)
  183. if err != nil {
  184. return err
  185. }
  186. return exec(
  187. context.Background(),
  188. *containerExecResponse.WebSocketURI,
  189. *containerExecResponse.Password,
  190. reader,
  191. writer,
  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.Writer)
  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 map[string]string) error {
  301. return cs.loginService.Login(ctx, params[login.TenantIDLoginParam])
  302. }
  303. func (cs *aciCloudService) CreateContextData(ctx context.Context, params map[string]string) (interface{}, string, error) {
  304. contextHelper := newContextCreateHelper()
  305. return contextHelper.createContextData(ctx, params)
  306. }