backend.go 15 KB

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