backend.go 11 KB

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