backend.go 16 KB

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