backend.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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 aci
  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"
  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. ecstypes "github.com/docker/ecs-plugin/pkg/compose"
  28. "github.com/docker/api/aci/convert"
  29. "github.com/docker/api/aci/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. backendType = store.AciContextType
  40. singleContainerTag = "docker-single-container"
  41. composeContainerTag = "docker-compose-application"
  42. composeContainerSeparator = "_"
  43. statusRunning = "Running"
  44. )
  45. // ContextParams options for creating ACI context
  46. type ContextParams struct {
  47. Description string
  48. Location string
  49. SubscriptionID string
  50. ResourceGroup string
  51. }
  52. // LoginParams azure login options
  53. type LoginParams struct {
  54. TenantID string
  55. ClientID string
  56. ClientSecret string
  57. }
  58. // Validate returns an error if options are not used properly
  59. func (opts LoginParams) Validate() error {
  60. if opts.ClientID != "" || opts.ClientSecret != "" {
  61. if opts.ClientID == "" || opts.ClientSecret == "" || opts.TenantID == "" {
  62. return errors.New("for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
  63. }
  64. }
  65. return nil
  66. }
  67. func init() {
  68. backend.Register(backendType, backendType, service, getCloudService)
  69. }
  70. func service(ctx context.Context) (backend.Service, error) {
  71. contextStore := store.ContextStore(ctx)
  72. currentContext := apicontext.CurrentContext(ctx)
  73. var aciContext store.AciContext
  74. if err := contextStore.GetEndpoint(currentContext, &aciContext); err != nil {
  75. return nil, err
  76. }
  77. return getAciAPIService(aciContext), nil
  78. }
  79. func getCloudService() (cloud.Service, error) {
  80. service, err := login.NewAzureLoginService()
  81. if err != nil {
  82. return nil, err
  83. }
  84. return &aciCloudService{
  85. loginService: service,
  86. }, nil
  87. }
  88. func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
  89. return &aciAPIService{
  90. aciContainerService: &aciContainerService{
  91. ctx: aciCtx,
  92. },
  93. aciComposeService: &aciComposeService{
  94. ctx: aciCtx,
  95. },
  96. }
  97. }
  98. type aciAPIService struct {
  99. *aciContainerService
  100. *aciComposeService
  101. }
  102. func (a *aciAPIService) ContainerService() containers.Service {
  103. return a.aciContainerService
  104. }
  105. func (a *aciAPIService) ComposeService() compose.Service {
  106. return a.aciComposeService
  107. }
  108. type aciContainerService struct {
  109. ctx store.AciContext
  110. }
  111. func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) {
  112. groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
  113. if err != nil {
  114. return nil, err
  115. }
  116. var containerGroups []containerinstance.ContainerGroup
  117. result, err := groupsClient.ListByResourceGroup(ctx, cs.ctx.ResourceGroup)
  118. if err != nil {
  119. return []containers.Container{}, err
  120. }
  121. for result.NotDone() {
  122. containerGroups = append(containerGroups, result.Values()...)
  123. if err := result.NextWithContext(ctx); err != nil {
  124. return []containers.Container{}, err
  125. }
  126. }
  127. var res []containers.Container
  128. for _, containerGroup := range containerGroups {
  129. group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, *containerGroup.Name)
  130. if err != nil {
  131. return []containers.Container{}, err
  132. }
  133. if group.Containers == nil || len(*group.Containers) < 1 {
  134. return []containers.Container{}, fmt.Errorf("no containers found in ACI container group %s", *containerGroup.Name)
  135. }
  136. for _, container := range *group.Containers {
  137. // don't list sidecar container
  138. if *container.Name == convert.ComposeDNSSidecarName {
  139. continue
  140. }
  141. if !all && convert.GetStatus(container, group) != statusRunning {
  142. continue
  143. }
  144. containerID := *containerGroup.Name + composeContainerSeparator + *container.Name
  145. if _, ok := group.Tags[singleContainerTag]; ok {
  146. containerID = *containerGroup.Name
  147. }
  148. c := convert.ContainerGroupToContainer(containerID, group, container)
  149. res = append(res, c)
  150. }
  151. }
  152. return res, nil
  153. }
  154. func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
  155. if strings.Contains(r.ID, composeContainerSeparator) {
  156. return errors.New(fmt.Sprintf("invalid container name. ACI container name cannot include %q", composeContainerSeparator))
  157. }
  158. project, err := convert.ContainerToComposeProject(r)
  159. if err != nil {
  160. return err
  161. }
  162. logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID)
  163. groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project)
  164. if err != nil {
  165. return err
  166. }
  167. addTag(&groupDefinition, singleContainerTag)
  168. return createACIContainers(ctx, cs.ctx, groupDefinition)
  169. }
  170. func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
  171. if groupDefinition.Tags == nil {
  172. groupDefinition.Tags = make(map[string]*string, 1)
  173. }
  174. groupDefinition.Tags[tagName] = to.StringPtr(tagName)
  175. }
  176. func (cs *aciContainerService) Start(ctx context.Context, containerID string) error {
  177. groupName, containerName := getGroupAndContainerName(containerID)
  178. if groupName != containerID {
  179. msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s"
  180. return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
  181. }
  182. containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
  183. if err != nil {
  184. return err
  185. }
  186. future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName)
  187. if err != nil {
  188. var aerr autorest.DetailedError
  189. if ok := errors.As(err, &aerr); ok {
  190. if aerr.StatusCode == http.StatusNotFound {
  191. return errdefs.ErrNotFound
  192. }
  193. }
  194. return err
  195. }
  196. return future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
  197. }
  198. func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error {
  199. if timeout != nil && *timeout != uint32(0) {
  200. return errors.Errorf("ACI integration does not support setting a timeout to stop a container before killing it.")
  201. }
  202. groupName, containerName := getGroupAndContainerName(containerID)
  203. if groupName != containerID {
  204. msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s"
  205. return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
  206. }
  207. return stopACIContainerGroup(ctx, cs.ctx, groupName)
  208. }
  209. func getGroupAndContainerName(containerID string) (string, string) {
  210. tokens := strings.Split(containerID, composeContainerSeparator)
  211. groupName := tokens[0]
  212. containerName := groupName
  213. if len(tokens) > 1 {
  214. containerName = tokens[len(tokens)-1]
  215. groupName = containerID[:len(containerID)-(len(containerName)+1)]
  216. }
  217. return groupName, containerName
  218. }
  219. func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
  220. err := verifyExecCommand(request.Command)
  221. if err != nil {
  222. return err
  223. }
  224. groupName, containerAciName := getGroupAndContainerName(name)
  225. containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
  226. if err != nil {
  227. return err
  228. }
  229. return exec(
  230. context.Background(),
  231. *containerExecResponse.WebSocketURI,
  232. *containerExecResponse.Password,
  233. request,
  234. )
  235. }
  236. func verifyExecCommand(command string) error {
  237. tokens := strings.Split(command, " ")
  238. if len(tokens) > 1 {
  239. return errors.New("ACI exec command does not accept arguments to the command. " +
  240. "Only the binary should be specified")
  241. }
  242. return nil
  243. }
  244. func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
  245. groupName, containerAciName := getGroupAndContainerName(containerName)
  246. var tail *int32
  247. if req.Follow {
  248. return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
  249. }
  250. if req.Tail != "all" {
  251. reqTail, err := strconv.Atoi(req.Tail)
  252. if err != nil {
  253. return err
  254. }
  255. i32 := int32(reqTail)
  256. tail = &i32
  257. }
  258. logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
  259. if err != nil {
  260. return err
  261. }
  262. _, err = fmt.Fprint(req.Writer, logs)
  263. return err
  264. }
  265. func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error {
  266. groupName, containerName := getGroupAndContainerName(containerID)
  267. if groupName != containerID {
  268. msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
  269. return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
  270. }
  271. if !request.Force {
  272. containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
  273. if err != nil {
  274. return err
  275. }
  276. cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName)
  277. if err != nil {
  278. if cg.StatusCode == http.StatusNotFound {
  279. return errdefs.ErrNotFound
  280. }
  281. return err
  282. }
  283. for _, container := range *cg.Containers {
  284. status := convert.GetStatus(container, cg)
  285. if status == statusRunning {
  286. return errdefs.ErrForbidden
  287. }
  288. }
  289. }
  290. cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
  291. // Delete returns `StatusNoContent` if the group is not found
  292. if cg.StatusCode == http.StatusNoContent {
  293. return errdefs.ErrNotFound
  294. }
  295. if err != nil {
  296. return err
  297. }
  298. return err
  299. }
  300. func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
  301. groupName, containerName := getGroupAndContainerName(containerID)
  302. cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
  303. if err != nil {
  304. return containers.Container{}, err
  305. }
  306. if cg.StatusCode == http.StatusNoContent {
  307. return containers.Container{}, errdefs.ErrNotFound
  308. }
  309. var cc containerinstance.Container
  310. var found = false
  311. for _, c := range *cg.Containers {
  312. if to.String(c.Name) == containerName {
  313. cc = c
  314. found = true
  315. break
  316. }
  317. }
  318. if !found {
  319. return containers.Container{}, errdefs.ErrNotFound
  320. }
  321. return convert.ContainerGroupToContainer(containerID, cg, cc), nil
  322. }
  323. type aciComposeService struct {
  324. ctx store.AciContext
  325. }
  326. func (cs *aciComposeService) Up(ctx context.Context, opts cli.ProjectOptions) error {
  327. project, err := cli.ProjectFromOptions(&opts)
  328. if err != nil {
  329. return err
  330. }
  331. logrus.Debugf("Up on project with name %q\n", project.Name)
  332. groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project)
  333. addTag(&groupDefinition, composeContainerTag)
  334. if err != nil {
  335. return err
  336. }
  337. return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
  338. }
  339. func (cs *aciComposeService) Down(ctx context.Context, opts cli.ProjectOptions) error {
  340. var project types.Project
  341. if opts.Name != "" {
  342. project = types.Project{Name: opts.Name}
  343. } else {
  344. fullProject, err := cli.ProjectFromOptions(&opts)
  345. if err != nil {
  346. return err
  347. }
  348. project = *fullProject
  349. }
  350. logrus.Debugf("Down on project with name %q\n", project.Name)
  351. cg, err := deleteACIContainerGroup(ctx, cs.ctx, project.Name)
  352. if err != nil {
  353. return err
  354. }
  355. if cg.StatusCode == http.StatusNoContent {
  356. return errdefs.ErrNotFound
  357. }
  358. return err
  359. }
  360. func (cs *aciComposeService) Ps(ctx context.Context, opts cli.ProjectOptions) ([]ecstypes.ServiceStatus, error) {
  361. return nil, errdefs.ErrNotImplemented
  362. }
  363. func (cs *aciComposeService) Logs(ctx context.Context, opts cli.ProjectOptions) error {
  364. return errdefs.ErrNotImplemented
  365. }
  366. type aciCloudService struct {
  367. loginService login.AzureLoginServiceAPI
  368. }
  369. func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
  370. opts, ok := params.(LoginParams)
  371. if !ok {
  372. return errors.New("Could not read azure LoginParams struct from generic parameter")
  373. }
  374. if opts.ClientID != "" {
  375. return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID)
  376. }
  377. return cs.loginService.Login(ctx, opts.TenantID)
  378. }
  379. func (cs *aciCloudService) Logout(ctx context.Context) error {
  380. return cs.loginService.Logout(ctx)
  381. }
  382. func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
  383. contextHelper := newContextCreateHelper()
  384. createOpts := params.(ContextParams)
  385. return contextHelper.createContextData(ctx, createOpts)
  386. }