volumes.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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/2019-12-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/context/store"
  25. "github.com/docker/compose-cli/api/progress"
  26. "github.com/docker/compose-cli/api/volumes"
  27. "github.com/docker/compose-cli/pkg/api"
  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.Name, *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 || opts == nil {
  71. return volumes.Volume{}, errors.New("could not read Azure VolumeCreateOptions struct from generic parameter")
  72. }
  73. w := progress.ContextWriter(ctx)
  74. w.Event(progress.NewEvent(opts.Account, progress.Working, "Validating"))
  75. accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
  76. if err != nil {
  77. w.Event(progress.ErrorEvent(opts.Account))
  78. return volumes.Volume{}, err
  79. }
  80. account, err := accountClient.GetProperties(ctx, cs.aciContext.ResourceGroup, opts.Account, "")
  81. if err == nil {
  82. w.Event(progress.NewEvent(opts.Account, progress.Done, "Use existing"))
  83. } else if !account.HasHTTPStatus(http.StatusNotFound) {
  84. w.Event(progress.ErrorEvent(opts.Account))
  85. return volumes.Volume{}, err
  86. } else {
  87. result, err := accountClient.CheckNameAvailability(ctx, storage.AccountCheckNameAvailabilityParameters{
  88. Name: to.StringPtr(opts.Account),
  89. Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
  90. })
  91. if err != nil {
  92. w.Event(progress.ErrorEvent(opts.Account))
  93. return volumes.Volume{}, err
  94. }
  95. if !*result.NameAvailable {
  96. w.Event(progress.ErrorEvent(opts.Account))
  97. return volumes.Volume{}, errors.New("error: " + *result.Message)
  98. }
  99. parameters := defaultStorageAccountParams(cs.aciContext)
  100. w.Event(progress.CreatingEvent(opts.Account))
  101. future, err := accountClient.Create(ctx, cs.aciContext.ResourceGroup, opts.Account, parameters)
  102. if err != nil {
  103. w.Event(progress.ErrorEvent(opts.Account))
  104. return volumes.Volume{}, err
  105. }
  106. if err := future.WaitForCompletionRef(ctx, accountClient.Client); err != nil {
  107. w.Event(progress.ErrorEvent(opts.Account))
  108. return volumes.Volume{}, err
  109. }
  110. account, err = future.Result(accountClient)
  111. if err != nil {
  112. w.Event(progress.ErrorEvent(opts.Account))
  113. return volumes.Volume{}, err
  114. }
  115. w.Event(progress.CreatedEvent(opts.Account))
  116. }
  117. w.Event(progress.CreatingEvent(name))
  118. fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
  119. if err != nil {
  120. return volumes.Volume{}, err
  121. }
  122. fileShare, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, *account.Name, name, "")
  123. if err == nil {
  124. w.Event(progress.ErrorEvent(name))
  125. return volumes.Volume{}, errors.Wrapf(api.ErrAlreadyExists, "Azure fileshare %q already exists", name)
  126. }
  127. if !fileShare.HasHTTPStatus(http.StatusNotFound) {
  128. w.Event(progress.ErrorEvent(name))
  129. return volumes.Volume{}, err
  130. }
  131. fileShare, err = fileShareClient.Create(ctx, cs.aciContext.ResourceGroup, *account.Name, name, storage.FileShare{})
  132. if err != nil {
  133. w.Event(progress.ErrorEvent(name))
  134. return volumes.Volume{}, err
  135. }
  136. w.Event(progress.CreatedEvent(name))
  137. return toVolume(*account.Name, *fileShare.Name), nil
  138. }
  139. func checkVolumeUsage(ctx context.Context, aciContext store.AciContext, id string) error {
  140. containerGroups, err := getACIContainerGroups(ctx, aciContext.SubscriptionID, aciContext.ResourceGroup)
  141. if err != nil {
  142. return err
  143. }
  144. for _, cg := range containerGroups {
  145. if hasVolume(cg.Volumes, id) {
  146. return errors.Errorf("volume %q is used in container group %q",
  147. id, *cg.Name)
  148. }
  149. }
  150. return nil
  151. }
  152. func hasVolume(volumes *[]containerinstance.Volume, id string) bool {
  153. if volumes == nil {
  154. return false
  155. }
  156. for _, v := range *volumes {
  157. if v.AzureFile != nil && v.AzureFile.StorageAccountName != nil && v.AzureFile.ShareName != nil &&
  158. (*v.AzureFile.StorageAccountName+"/"+*v.AzureFile.ShareName) == id {
  159. return true
  160. }
  161. }
  162. return false
  163. }
  164. func (cs *aciVolumeService) Delete(ctx context.Context, id string, options interface{}) error {
  165. err := checkVolumeUsage(ctx, cs.aciContext, id)
  166. if err != nil {
  167. return err
  168. }
  169. storageAccount, fileshare, err := getStorageAccountAndFileshare(id)
  170. if err != nil {
  171. return err
  172. }
  173. fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
  174. if err != nil {
  175. return err
  176. }
  177. fileShareItemsPage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, storageAccount, "", "", "")
  178. if err != nil {
  179. return err
  180. }
  181. fileshares := fileShareItemsPage.Values()
  182. if len(fileshares) == 1 && *fileshares[0].Name == fileshare {
  183. storageAccountsClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
  184. if err != nil {
  185. return err
  186. }
  187. account, err := storageAccountsClient.GetProperties(ctx, cs.aciContext.ResourceGroup, storageAccount, "")
  188. if err != nil {
  189. return err
  190. }
  191. if err == nil {
  192. if _, ok := account.Tags[dockerVolumeTag]; ok {
  193. result, err := storageAccountsClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount)
  194. if result.IsHTTPStatus(http.StatusNoContent) {
  195. return errors.Wrapf(api.ErrNotFound, "storage account %s does not exist", storageAccount)
  196. }
  197. return err
  198. }
  199. }
  200. }
  201. result, err := fileShareClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshare)
  202. if result.HasHTTPStatus(http.StatusNoContent) {
  203. return errors.Wrapf(api.ErrNotFound, "fileshare %q", fileshare)
  204. }
  205. return err
  206. }
  207. func (cs *aciVolumeService) Inspect(ctx context.Context, id string) (volumes.Volume, error) {
  208. storageAccount, fileshareName, err := getStorageAccountAndFileshare(id)
  209. if err != nil {
  210. return volumes.Volume{}, err
  211. }
  212. fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
  213. if err != nil {
  214. return volumes.Volume{}, err
  215. }
  216. res, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshareName, "")
  217. if err != nil { // Just checks if it exists
  218. if res.HasHTTPStatus(http.StatusNotFound) {
  219. return volumes.Volume{}, errors.Wrapf(api.ErrNotFound, "account %q, file share %q. Original message %s", storageAccount, fileshareName, err.Error())
  220. }
  221. return volumes.Volume{}, err
  222. }
  223. return toVolume(storageAccount, fileshareName), nil
  224. }
  225. func toVolume(storageAccountName string, fileShareName string) volumes.Volume {
  226. return volumes.Volume{
  227. ID: volumeID(storageAccountName, fileShareName),
  228. Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, storageAccountName),
  229. }
  230. }
  231. func volumeID(storageAccount string, fileShareName string) string {
  232. return fmt.Sprintf("%s/%s", storageAccount, fileShareName)
  233. }
  234. func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters {
  235. tags := map[string]*string{dockerVolumeTag: to.StringPtr(dockerVolumeTag)}
  236. return storage.AccountCreateParameters{
  237. Location: to.StringPtr(aciContext.Location),
  238. Sku: &storage.Sku{
  239. Name: storage.StandardLRS,
  240. },
  241. Tags: tags,
  242. }
  243. }
  244. func getStorageAccountAndFileshare(volumeID string) (string, string, error) {
  245. tokens := strings.Split(volumeID, "/")
  246. if len(tokens) != 2 {
  247. return "", "", errors.New("invalid format for volume ID, expected storageaccount/fileshare")
  248. }
  249. return tokens[0], tokens[1], nil
  250. }