volumes.go 7.3 KB

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