backend.go 12 KB

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