diff --git a/aci/backend.go b/aci/backend.go index 5e374d492..19e386646 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -35,8 +35,8 @@ import ( "github.com/docker/compose-cli/aci/login" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" - "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/api/secrets" + "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" apicontext "github.com/docker/compose-cli/context" "github.com/docker/compose-cli/context/cloud" @@ -110,6 +110,9 @@ func getAciAPIService(aciCtx store.AciContext) *aciAPIService { aciComposeService: &aciComposeService{ ctx: aciCtx, }, + aciVolumeService: &aciVolumeService{ + ctx: aciCtx, + }, } } @@ -169,20 +172,20 @@ func getContainerGroups(ctx context.Context, subscriptionID string, resourceGrou var containerGroups []containerinstance.ContainerGroup result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup) if err != nil { - return []containerinstance.ContainerGroup{}, err + return nil, err } for result.NotDone() { containerGroups = append(containerGroups, result.Values()...) if err := result.NextWithContext(ctx); err != nil { - return []containerinstance.ContainerGroup{}, err + return nil, err } } var groups []containerinstance.ContainerGroup for _, group := range containerGroups { group, err := groupsClient.Get(ctx, resourceGroup, *group.Name) if err != nil { - return []containerinstance.ContainerGroup{}, err + return nil, err } groups = append(groups, group) } @@ -507,11 +510,23 @@ type aciVolumeService struct { } func (cs *aciVolumeService) List(ctx context.Context) ([]volumes.Volume, error) { - return nil, nil + storageHelper := login.StorageAccountHelper{AciContext: cs.ctx} + return storageHelper.ListFileShare(ctx) +} + +//VolumeCreateOptions options to create a new ACI volume +type VolumeCreateOptions struct { + Account string + Fileshare string } func (cs *aciVolumeService) Create(ctx context.Context, options interface{}) (volumes.Volume, error) { - return volumes.Volume{}, nil + opts, ok := options.(VolumeCreateOptions) + if !ok { + return volumes.Volume{}, errors.New("Could not read azure LoginParams struct from generic parameter") + } + storageHelper := login.StorageAccountHelper{AciContext: cs.ctx} + return storageHelper.CreateFileShare(ctx, opts.Account, opts.Fileshare) } type aciCloudService struct { diff --git a/aci/convert/convert.go b/aci/convert/convert.go index eb505b4c1..65ce5683b 100644 --- a/aci/convert/convert.go +++ b/aci/convert/convert.go @@ -56,13 +56,8 @@ const ( func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project) (containerinstance.ContainerGroup, error) { project := projectAciHelper(p) containerGroupName := strings.ToLower(project.Name) - loginService, err := login.NewAzureLoginService() - if err != nil { - return containerinstance.ContainerGroup{}, err - } storageHelper := login.StorageAccountHelper{ - LoginService: *loginService, - AciContext: aciContext, + AciContext: aciContext, } volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper) if err != nil { diff --git a/aci/login/client.go b/aci/login/client.go index 95e8160c1..7de5d327e 100644 --- a/aci/login/client.go +++ b/aci/login/client.go @@ -67,7 +67,7 @@ func NewStorageAccountsClient(subscriptionID string) (storage.AccountsClient, er return containerGroupsClient, nil } -// NewStorageAccountsClient get client to manipulate storage accounts +// NewFileShareClient get client to manipulate file shares func NewFileShareClient(subscriptionID string) (storage.FileSharesClient, error) { containerGroupsClient := storage.NewFileSharesClient(subscriptionID) err := setupClient(&containerGroupsClient.Client) @@ -80,7 +80,6 @@ func NewFileShareClient(subscriptionID string) (storage.FileSharesClient, error) return containerGroupsClient, nil } - // NewSubscriptionsClient get subscription client func NewSubscriptionsClient() (subscription.SubscriptionsClient, error) { subc := subscription.NewSubscriptionsClient() diff --git a/aci/login/storage_helper.go b/aci/login/storage_helper.go index 8e16b1f4c..de81c5b31 100644 --- a/aci/login/storage_helper.go +++ b/aci/login/storage_helper.go @@ -19,8 +19,12 @@ package login import ( "context" "fmt" + "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" + "github.com/docker/compose-cli/api/volumes" + "github.com/docker/compose-cli/errdefs" + "github.com/pkg/errors" "github.com/docker/compose-cli/context/store" @@ -28,8 +32,7 @@ import ( // StorageAccountHelper helper for Azure Storage Account type StorageAccountHelper struct { - LoginService AzureLoginService - AciContext store.AciContext + AciContext store.AciContext } // GetAzureStorageAccountKey retrieves the storage account ket from the current azure login @@ -50,61 +53,99 @@ func (helper StorageAccountHelper) GetAzureStorageAccountKey(ctx context.Context return *key.Value, nil } -func (helper StorageAccountHelper) ListFileShare(ctx context.Context) ([]string, error) { +// ListFileShare list file shares in all visible storage accounts +func (helper StorageAccountHelper) ListFileShare(ctx context.Context) ([]volumes.Volume, error) { aciContext := helper.AciContext accountClient, err := NewStorageAccountsClient(aciContext.SubscriptionID) if err != nil { return nil, err } result, err := accountClient.ListByResourceGroup(ctx, aciContext.ResourceGroup) + if err != nil { + return nil, err + } accounts := result.Value fileShareClient, err := NewFileShareClient(aciContext.SubscriptionID) - fileShares := []string{} + if err != nil { + return nil, err + } + fileShares := []volumes.Volume{} for _, account := range *accounts { fileSharePage, err := fileShareClient.List(ctx, aciContext.ResourceGroup, *account.Name, "", "", "") if err != nil { return nil, err } - for ; fileSharePage.NotDone() ; fileSharePage.NextWithContext(ctx) { + + for fileSharePage.NotDone() { values := fileSharePage.Values() for _, fileShare := range values { - fileShares = append(fileShares, *fileShare.Name) + fileShares = append(fileShares, toVolume(account, *fileShare.Name)) + } + if err := fileSharePage.NextWithContext(ctx); err != nil { + return nil, err } } } return fileShares, nil } -func (helper StorageAccountHelper) CreateFileShare(ctx context.Context, accountName string, fileShareName string) (storage.FileShare, error) { +func toVolume(account storage.Account, fileShareName string) volumes.Volume { + return volumes.Volume{ + ID: fmt.Sprintf("%s@%s", *account.Name, fileShareName), + Name: fileShareName, + Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name), + } +} + +// CreateFileShare create a new fileshare +func (helper StorageAccountHelper) CreateFileShare(ctx context.Context, accountName string, fileShareName string) (volumes.Volume, error) { aciContext := helper.AciContext accountClient, err := NewStorageAccountsClient(aciContext.SubscriptionID) if err != nil { - return storage.FileShare{}, err + return volumes.Volume{}, err } account, err := accountClient.GetProperties(ctx, aciContext.ResourceGroup, accountName, "") if err != nil { - //TODO check err not found - parameters := storage.AccountCreateParameters{ - Location: &aciContext.Location, - Sku:&storage.Sku{ - Name: storage.StandardLRS, - Tier: storage.Standard, - }, + if account.StatusCode != 404 { + return volumes.Volume{}, err } + //TODO confirm storage account creation + parameters := defaultStorageAccountParams(aciContext) // TODO progress account creation future, err := accountClient.Create(ctx, aciContext.ResourceGroup, accountName, parameters) if err != nil { - return storage.FileShare{}, err + return volumes.Volume{}, err } account, err = future.Result(accountClient) + if err != nil { + return volumes.Volume{}, err + } } fileShareClient, err := NewFileShareClient(aciContext.SubscriptionID) - fileShare, err := fileShareClient.Get(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, "") if err != nil { - // TODO check err not found - fileShare, err = fileShareClient.Create(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, storage.FileShare{}) + return volumes.Volume{}, err } - return fileShare, nil + fileShare, err := fileShareClient.Get(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, "") + if err == nil { + return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", fileShareName) + } + if fileShare.StatusCode != 404 { + return volumes.Volume{}, err + } + fileShare, err = fileShareClient.Create(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, storage.FileShare{}) + if err != nil { + return volumes.Volume{}, err + } + return toVolume(account, *fileShare.Name), nil } +func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters { + return storage.AccountCreateParameters{ + Location: &aciContext.Location, + Sku: &storage.Sku{ + Name: storage.StandardLRS, + Tier: storage.Standard, + }, + } +} diff --git a/api/client/client.go b/api/client/client.go index 937aad141..632dc3256 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -18,6 +18,7 @@ package client import ( "context" + "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/api/compose" @@ -87,6 +88,7 @@ func (c *Client) SecretsService() secrets.Service { return &secretsService{} } + // VolumeService returns the backend service for the current context func (c *Client) VolumeService() volumes.Service { if vs := c.bs.VolumeService(); vs != nil { diff --git a/api/client/volume.go b/api/client/volume.go index 37ebfac38..34adfae9e 100644 --- a/api/client/volume.go +++ b/api/client/volume.go @@ -18,6 +18,7 @@ package client import ( "context" + "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/errdefs" ) @@ -31,6 +32,6 @@ func (c *volumeService) List(ctx context.Context) ([]volumes.Volume, error) { } // Create creates a volume -func (c *volumeService) Create(ctx context.Context, options interface {}) (volumes.Volume, error) { +func (c *volumeService) Create(ctx context.Context, options interface{}) (volumes.Volume, error) { return volumes.Volume{}, errdefs.ErrNotImplemented } diff --git a/api/volumes/api.go b/api/volumes/api.go index ce6efe04a..59a8b1d71 100644 --- a/api/volumes/api.go +++ b/api/volumes/api.go @@ -20,13 +20,11 @@ import ( "context" ) +// Volume volume info type Volume struct { - Name string -} - -type VolumeCreateOptions struct { - account string - fileshare string + ID string + Name string + Description string } // Service interacts with the underlying container backend diff --git a/cli/cmd/volume/create.go b/cli/cmd/volume/create.go index 3a3a0af30..07b2ebcf6 100644 --- a/cli/cmd/volume/create.go +++ b/cli/cmd/volume/create.go @@ -18,17 +18,21 @@ package volume import ( "fmt" - "github.com/docker/compose-cli/api/client" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/docker/compose-cli/aci" + "github.com/spf13/cobra" + + "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/volumes" ) -type createVolumeOptions struct { - Account string - Fileshare string -} - -// SecretCommand manage secrets -func VolumeCommand() *cobra.Command { +// Command manage volumes +func Command() *cobra.Command { cmd := &cobra.Command{ Use: "volume", Short: "Manages volumes", @@ -36,16 +40,17 @@ func VolumeCommand() *cobra.Command { cmd.AddCommand( createVolume(), + listVolume(), ) return cmd } func createVolume() *cobra.Command { - opts := createVolumeOptions{} + opts := aci.VolumeCreateOptions{} cmd := &cobra.Command{ Use: "create", Short: "Creates an Azure file share to use as ACI volume.", - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { c, err := client.New(cmd.Context()) if err != nil { @@ -60,7 +65,43 @@ func createVolume() *cobra.Command { }, } - cmd.Flags().StringVar(&opts.Account, "storage-account", "", "Storage account name") + cmd.Flags().StringVar(&opts.Account, "storage-account", "", "Storage account name") cmd.Flags().StringVar(&opts.Fileshare, "fileshare", "", "Fileshare name") return cmd } + +func listVolume() *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Short: "list Azure file shares usable as ACI volumes.", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := client.New(cmd.Context()) + if err != nil { + return err + } + vols, err := c.VolumeService().List(cmd.Context()) + if err != nil { + return err + } + printList(os.Stdout, vols) + return nil + }, + } + return cmd +} + +func printList(out io.Writer, volumes []volumes.Volume) { + printSection(out, func(w io.Writer) { + for _, vol := range volumes { + fmt.Fprintf(w, "%s\t%s\t%s\n", vol.ID, vol.Name, vol.Description) // nolint:errcheck + } + }, "ID", "NAME", "DESCRIPTION") +} + +func printSection(out io.Writer, printer func(io.Writer), headers ...string) { + w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) + fmt.Fprintln(w, strings.Join(headers, "\t")) // nolint:errcheck + printer(w) + w.Flush() // nolint:errcheck +} diff --git a/cli/main.go b/cli/main.go index 79167ac4b..69d84324d 100644 --- a/cli/main.go +++ b/cli/main.go @@ -19,7 +19,6 @@ package main import ( "context" "fmt" - volume "github.com/docker/compose-cli/cli/cmd/volume" "math/rand" "os" "os/signal" @@ -28,6 +27,8 @@ import ( "syscall" "time" + volume "github.com/docker/compose-cli/cli/cmd/volume" + "github.com/docker/compose-cli/cli/cmd/compose" "github.com/docker/compose-cli/cli/cmd/logout" @@ -134,7 +135,7 @@ func main() { // Place holders cmd.EcsCommand(), - volume.VolumeCommand(), + volume.Command(), ) helpFunc := root.HelpFunc() diff --git a/ecs/backend.go b/ecs/backend.go index 8252ad28d..a615cbeb4 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -18,6 +18,7 @@ package ecs import ( "context" + "github.com/docker/compose-cli/api/volumes" "github.com/aws/aws-sdk-go/aws" diff --git a/ecs/local/backend.go b/ecs/local/backend.go index f0c68ab89..cec7c104f 100644 --- a/ecs/local/backend.go +++ b/ecs/local/backend.go @@ -18,12 +18,14 @@ package local import ( "context" + "github.com/docker/compose-cli/api/volumes" + "github.com/docker/docker/client" + "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" "github.com/docker/compose-cli/api/secrets" - "github.com/docker/docker/client" "github.com/docker/compose-cli/backend" "github.com/docker/compose-cli/context/cloud" diff --git a/local/backend.go b/local/backend.go index 99e2afc50..dfd8f6d4b 100644 --- a/local/backend.go +++ b/local/backend.go @@ -38,8 +38,8 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" - "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/api/secrets" + "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" "github.com/docker/compose-cli/context/cloud" "github.com/docker/compose-cli/errdefs" diff --git a/tests/ecs-local-e2e/context_test.go b/tests/ecs-local-e2e/context_test.go index 6bf26bb73..4d0003ae0 100644 --- a/tests/ecs-local-e2e/context_test.go +++ b/tests/ecs-local-e2e/context_test.go @@ -21,8 +21,9 @@ import ( "os" "testing" - . "github.com/docker/compose-cli/tests/framework" "gotest.tools/v3/icmd" + + . "github.com/docker/compose-cli/tests/framework" ) const (