volumes.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  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. "strings"
  19. "github.com/pkg/errors"
  20. "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
  21. "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
  22. "github.com/Azure/go-autorest/autorest/to"
  23. "github.com/docker/compose-cli/aci/login"
  24. "github.com/docker/compose-cli/api/volumes"
  25. "github.com/docker/compose-cli/context/store"
  26. "github.com/docker/compose-cli/errdefs"
  27. "github.com/docker/compose-cli/progress"
  28. )
  29. type aciVolumeService struct {
  30. aciContext store.AciContext
  31. }
  32. func (cs *aciVolumeService) List(ctx context.Context) ([]volumes.Volume, error) {
  33. accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
  34. if err != nil {
  35. return nil, err
  36. }
  37. result, err := accountClient.ListByResourceGroup(ctx, cs.aciContext.ResourceGroup)
  38. if err != nil {
  39. return nil, err
  40. }
  41. accounts := result.Value
  42. fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
  43. if err != nil {
  44. return nil, err
  45. }
  46. fileShares := []volumes.Volume{}
  47. for _, account := range *accounts {
  48. fileSharePage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, *account.Name, "", "", "")
  49. if err != nil {
  50. return nil, err
  51. }
  52. for fileSharePage.NotDone() {
  53. values := fileSharePage.Values()
  54. for _, fileShare := range values {
  55. fileShares = append(fileShares, toVolume(account, *fileShare.Name))
  56. }
  57. if err := fileSharePage.NextWithContext(ctx); err != nil {
  58. return nil, err
  59. }
  60. }
  61. }
  62. return fileShares, nil
  63. }
  64. // VolumeCreateOptions options to create a new ACI volume
  65. type VolumeCreateOptions struct {
  66. Account string
  67. }
  68. func (cs *aciVolumeService) Create(ctx context.Context, name string, options interface{}) (volumes.Volume, error) {
  69. opts, ok := options.(VolumeCreateOptions)
  70. if !ok {
  71. return volumes.Volume{}, errors.New("could not read Azure VolumeCreateOptions struct from generic parameter")
  72. }
  73. w := progress.ContextWriter(ctx)
  74. w.Event(event(opts.Account, progress.Working, "Validating"))
  75. accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
  76. if err != nil {
  77. return volumes.Volume{}, err
  78. }
  79. account, err := accountClient.GetProperties(ctx, cs.aciContext.ResourceGroup, opts.Account, "")
  80. if err == nil {
  81. w.Event(event(opts.Account, progress.Done, "Use existing"))
  82. } else if !account.HasHTTPStatus(http.StatusNotFound) {
  83. return volumes.Volume{}, err
  84. } else {
  85. result, err := accountClient.CheckNameAvailability(ctx, storage.AccountCheckNameAvailabilityParameters{
  86. Name: to.StringPtr(opts.Account),
  87. Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
  88. })
  89. if err != nil {
  90. return volumes.Volume{}, err
  91. }
  92. if !*result.NameAvailable {
  93. return volumes.Volume{}, errors.New("error: " + *result.Message)
  94. }
  95. parameters := defaultStorageAccountParams(cs.aciContext)
  96. w.Event(event(opts.Account, progress.Working, "Creating"))
  97. future, err := accountClient.Create(ctx, cs.aciContext.ResourceGroup, opts.Account, parameters)
  98. if err != nil {
  99. w.Event(errorEvent(opts.Account))
  100. return volumes.Volume{}, err
  101. }
  102. if err := future.WaitForCompletionRef(ctx, accountClient.Client); err != nil {
  103. w.Event(errorEvent(opts.Account))
  104. return volumes.Volume{}, err
  105. }
  106. account, err = future.Result(accountClient)
  107. if err != nil {
  108. w.Event(errorEvent(opts.Account))
  109. return volumes.Volume{}, err
  110. }
  111. w.Event(event(opts.Account, progress.Done, "Created"))
  112. }
  113. w.Event(event(name, progress.Working, "Creating"))
  114. fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
  115. if err != nil {
  116. return volumes.Volume{}, err
  117. }
  118. fileShare, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, *account.Name, name, "")
  119. if err == nil {
  120. w.Event(errorEvent(name))
  121. return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", name)
  122. }
  123. if !fileShare.HasHTTPStatus(http.StatusNotFound) {
  124. w.Event(errorEvent(name))
  125. return volumes.Volume{}, err
  126. }
  127. fileShare, err = fileShareClient.Create(ctx, cs.aciContext.ResourceGroup, *account.Name, name, storage.FileShare{})
  128. if err != nil {
  129. w.Event(errorEvent(name))
  130. return volumes.Volume{}, err
  131. }
  132. w.Event(event(name, progress.Done, "Created"))
  133. return toVolume(account, *fileShare.Name), nil
  134. }
  135. func event(resource string, status progress.EventStatus, text string) progress.Event {
  136. return progress.Event{
  137. ID: resource,
  138. Status: status,
  139. StatusText: text,
  140. }
  141. }
  142. func errorEvent(resource string) progress.Event {
  143. return progress.Event{
  144. ID: resource,
  145. Status: progress.Error,
  146. StatusText: "Error",
  147. }
  148. }
  149. func checkVolumeUsage(ctx context.Context, aciContext store.AciContext, id string) error {
  150. containerGroups, err := getACIContainerGroups(ctx, aciContext.SubscriptionID, aciContext.ResourceGroup)
  151. if err != nil {
  152. return err
  153. }
  154. for _, cg := range containerGroups {
  155. if hasVolume(cg.Volumes, id) {
  156. return errors.Errorf("volume %q is used in container group %q",
  157. id, *cg.Name)
  158. }
  159. }
  160. return nil
  161. }
  162. func hasVolume(volumes *[]containerinstance.Volume, id string) bool {
  163. if volumes == nil {
  164. return false
  165. }
  166. for _, v := range *volumes {
  167. if v.AzureFile != nil && v.AzureFile.StorageAccountName != nil && v.AzureFile.ShareName != nil &&
  168. (*v.AzureFile.StorageAccountName+"/"+*v.AzureFile.ShareName) == id {
  169. return true
  170. }
  171. }
  172. return false
  173. }
  174. func (cs *aciVolumeService) Delete(ctx context.Context, id string, options interface{}) error {
  175. err := checkVolumeUsage(ctx, cs.aciContext, id)
  176. if err != nil {
  177. return err
  178. }
  179. tokens := strings.Split(id, "/")
  180. if len(tokens) != 2 {
  181. return errors.New("invalid format for volume ID, expected storageaccount/fileshare")
  182. }
  183. storageAccount := tokens[0]
  184. fileshare := tokens[1]
  185. fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
  186. if err != nil {
  187. return err
  188. }
  189. fileShareItemsPage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, storageAccount, "", "", "")
  190. if err != nil {
  191. return err
  192. }
  193. fileshares := fileShareItemsPage.Values()
  194. if len(fileshares) == 1 && *fileshares[0].Name == fileshare {
  195. storageAccountsClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
  196. if err != nil {
  197. return err
  198. }
  199. account, err := storageAccountsClient.GetProperties(ctx, cs.aciContext.ResourceGroup, storageAccount, "")
  200. if err != nil {
  201. return err
  202. }
  203. if err == nil {
  204. if _, ok := account.Tags[dockerVolumeTag]; ok {
  205. result, err := storageAccountsClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount)
  206. if result.StatusCode == http.StatusNoContent {
  207. return errors.Wrapf(errdefs.ErrNotFound, "storage account %s does not exist", storageAccount)
  208. }
  209. return err
  210. }
  211. }
  212. }
  213. result, err := fileShareClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshare)
  214. if result.StatusCode == 204 {
  215. return errors.Wrapf(errdefs.ErrNotFound, "fileshare %q", fileshare)
  216. }
  217. return err
  218. }
  219. func toVolume(account storage.Account, fileShareName string) volumes.Volume {
  220. return volumes.Volume{
  221. ID: volumeID(*account.Name, fileShareName),
  222. Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name),
  223. }
  224. }
  225. func volumeID(storageAccount string, fileShareName string) string {
  226. return fmt.Sprintf("%s/%s", storageAccount, fileShareName)
  227. }
  228. func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters {
  229. tags := map[string]*string{dockerVolumeTag: to.StringPtr(dockerVolumeTag)}
  230. return storage.AccountCreateParameters{
  231. Location: to.StringPtr(aciContext.Location),
  232. Sku: &storage.Sku{
  233. Name: storage.StandardLRS,
  234. },
  235. Tags: tags,
  236. }
  237. }