Browse Source

Add auto creation of Azure volumes during `compose up`

Signed-off-by: Ulysses Souza <[email protected]>
Ulysses Souza 5 years ago
parent
commit
3fbb9bd864
6 changed files with 107 additions and 34 deletions
  1. 36 0
      aci/aci.go
  2. 32 1
      aci/compose.go
  3. 13 10
      aci/convert/volume.go
  4. 2 2
      aci/convert/volume_test.go
  5. 5 2
      aci/volumes.go
  6. 19 19
      tests/aci-e2e/e2e-aci_test.go

+ 36 - 0
aci/aci.go

@@ -28,6 +28,7 @@ import (
 	"github.com/Azure/go-autorest/autorest"
 	"github.com/Azure/go-autorest/autorest/to"
 	tm "github.com/buger/goterm"
+	"github.com/compose-spec/compose-go/types"
 	"github.com/gobwas/ws"
 	"github.com/gobwas/ws/wsutil"
 	"github.com/morikuni/aec"
@@ -35,6 +36,7 @@ import (
 
 	"github.com/docker/compose-cli/aci/convert"
 	"github.com/docker/compose-cli/aci/login"
+	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/context/store"
 	"github.com/docker/compose-cli/errdefs"
@@ -64,12 +66,46 @@ func createACIContainers(ctx context.Context, aciContext store.AciContext, group
 	return createOrUpdateACIContainers(ctx, aciContext, groupDefinition)
 }
 
+func autocreateFileshares(ctx context.Context, project *types.Project) error {
+	clt, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+	for _, v := range project.Volumes {
+		if v.Driver != convert.AzureFileDriverName {
+			return fmt.Errorf("cannot use ACI volume, required driver is %q, found %q", convert.AzureFileDriverName, v.Driver)
+		}
+		shareName, ok := v.DriverOpts[convert.VolumeDriveroptsShareNameKey]
+		if !ok {
+			return fmt.Errorf("cannot retrieve fileshare name for Azure file share")
+		}
+		accountName, ok := v.DriverOpts[convert.VolumeDriveroptsAccountNameKey]
+		if !ok {
+			return fmt.Errorf("cannot retrieve account name for Azure file share")
+		}
+		_, err = clt.VolumeService().Inspect(ctx, fmt.Sprintf("%s/%s", accountName, shareName))
+		if err != nil { // Not found, autocreate fileshare
+			if !errdefs.IsNotFoundError(err) {
+				return err
+			}
+			aciVolumeOpts := &VolumeCreateOptions{
+				Account: accountName,
+			}
+			if _, err = clt.VolumeService().Create(ctx, shareName, aciVolumeOpts); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
 func createOrUpdateACIContainers(ctx context.Context, aciContext store.AciContext, groupDefinition containerinstance.ContainerGroup) error {
 	w := progress.ContextWriter(ctx)
 	containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
 	if err != nil {
 		return errors.Wrapf(err, "cannot get container group client")
 	}
+
 	groupDisplay := "Group " + *groupDefinition.Name
 	w.Event(progress.Event{
 		ID:         groupDisplay,

+ 32 - 1
aci/compose.go

@@ -46,18 +46,49 @@ func newComposeService(ctx store.AciContext) aciComposeService {
 
 func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error {
 	logrus.Debugf("Up on project with name %q", project.Name)
+
+	if err := autocreateFileshares(ctx, project); err != nil {
+		return err
+	}
+
 	groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project, cs.storageLogin)
+	if err != nil {
+		return err
+	}
+
 	addTag(&groupDefinition, composeContainerTag)
+	return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
+}
 
+func (cs aciComposeService) warnKeepVolumeOnDown(ctx context.Context, projectName string) error {
+	cgClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
 	if err != nil {
 		return err
 	}
-	return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
+	cg, err := cgClient.Get(ctx, cs.ctx.ResourceGroup, projectName)
+	if err != nil {
+		return err
+	}
+	if cg.Volumes == nil {
+		return nil
+	}
+	for _, v := range *cg.Volumes {
+		if v.AzureFile == nil || v.AzureFile.StorageAccountName == nil || v.AzureFile.ShareName == nil {
+			continue
+		}
+		fmt.Printf("WARNING: fileshare \"%s/%s\" will NOT be deleted. Use 'docker volume rm' if you want to delete this volume\n",
+			*v.AzureFile.StorageAccountName, *v.AzureFile.ShareName)
+	}
+	return nil
 }
 
 func (cs *aciComposeService) Down(ctx context.Context, project string) error {
 	logrus.Debugf("Down on project with name %q", project)
 
+	if err := cs.warnKeepVolumeOnDown(ctx, project); err != nil {
+		return err
+	}
+
 	cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
 	if err != nil {
 		return err

+ 13 - 10
aci/convert/volume.go

@@ -32,21 +32,24 @@ import (
 )
 
 const (
-	azureFileDriverName            = "azure_file"
-	volumeDriveroptsShareNameKey   = "share_name"
-	volumeDriveroptsAccountNameKey = "storage_account_name"
+	// AzureFileDriverName driver name for azure file share
+	AzureFileDriverName = "azure_file"
+	// VolumeDriveroptsShareNameKey driver opt for fileshare name
+	VolumeDriveroptsShareNameKey = "share_name"
+	// VolumeDriveroptsAccountNameKey driver opt for storage account
+	VolumeDriveroptsAccountNameKey = "storage_account_name"
 	volumeReadOnly                 = "read_only"
 )
 
 func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageLogin) ([]containerinstance.Volume, error) {
 	var azureFileVolumesSlice []containerinstance.Volume
 	for name, v := range p.Volumes {
-		if v.Driver == azureFileDriverName {
-			shareName, ok := v.DriverOpts[volumeDriveroptsShareNameKey]
+		if v.Driver == AzureFileDriverName {
+			shareName, ok := v.DriverOpts[VolumeDriveroptsShareNameKey]
 			if !ok {
 				return nil, fmt.Errorf("cannot retrieve fileshare name for Azurefile")
 			}
-			accountName, ok := v.DriverOpts[volumeDriveroptsAccountNameKey]
+			accountName, ok := v.DriverOpts[VolumeDriveroptsAccountNameKey]
 			if !ok {
 				return nil, fmt.Errorf("cannot retrieve account name for Azurefile")
 			}
@@ -105,15 +108,15 @@ func GetRunVolumes(volumes []string) (map[string]types.VolumeConfig, []types.Ser
 		readOnly := strconv.FormatBool(vi.readonly)
 		projectVolumes[vi.name] = types.VolumeConfig{
 			Name:   vi.name,
-			Driver: azureFileDriverName,
+			Driver: AzureFileDriverName,
 			DriverOpts: map[string]string{
-				volumeDriveroptsAccountNameKey: vi.storageAccount,
-				volumeDriveroptsShareNameKey:   vi.fileshare,
+				VolumeDriveroptsAccountNameKey: vi.storageAccount,
+				VolumeDriveroptsShareNameKey:   vi.fileshare,
 				volumeReadOnly:                 readOnly,
 			},
 		}
 		sv := types.ServiceVolumeConfig{
-			Type:     azureFileDriverName,
+			Type:     AzureFileDriverName,
 			Source:   vi.name,
 			Target:   vi.target,
 			ReadOnly: vi.readonly,

+ 2 - 2
aci/convert/volume_test.go

@@ -207,8 +207,8 @@ func getAzurefileVolumeConfig(name string, accountNameKey string, shareNameKey s
 		Name:   name,
 		Driver: "azure_file",
 		DriverOpts: map[string]string{
-			volumeDriveroptsAccountNameKey: accountNameKey,
-			volumeDriveroptsShareNameKey:   shareNameKey,
+			VolumeDriveroptsAccountNameKey: accountNameKey,
+			VolumeDriveroptsShareNameKey:   shareNameKey,
 			volumeReadOnly:                 strconv.FormatBool(readOnly),
 		},
 	}

+ 5 - 2
aci/volumes.go

@@ -232,7 +232,7 @@ func (cs *aciVolumeService) Delete(ctx context.Context, id string, options inter
 	}
 
 	result, err := fileShareClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshare)
-	if result.StatusCode == 204 {
+	if result.HasHTTPStatus(http.StatusNoContent) {
 		return errors.Wrapf(errdefs.ErrNotFound, "fileshare %q", fileshare)
 	}
 	return err
@@ -247,8 +247,11 @@ func (cs *aciVolumeService) Inspect(ctx context.Context, id string) (volumes.Vol
 	if err != nil {
 		return volumes.Volume{}, err
 	}
-	_, err = fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshareName, "")
+	res, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshareName, "")
 	if err != nil { // Just checks if it exists
+		if res.HasHTTPStatus(http.StatusNotFound) {
+			return volumes.Volume{}, errors.Wrapf(errdefs.ErrNotFound, "account %q, file share %q. Original message %s", storageAccount, fileshareName, err.Error())
+		}
 		return volumes.Volume{}, err
 	}
 	return toVolume(storageAccount, fileshareName), nil

+ 19 - 19
tests/aci-e2e/e2e-aci_test.go

@@ -644,9 +644,7 @@ func TestUpUpdate(t *testing.T) {
 	)
 	c := NewParallelE2eCLI(t, binDir)
 	sID, groupID, location := setupTestResourceGroup(t, c)
-	composeAccountName := groupID + "-sa"
-	composeAccountName = strings.ReplaceAll(composeAccountName, "-", "")
-	composeAccountName = strings.ToLower(composeAccountName)
+	composeAccountName := strings.ToLower(strings.ReplaceAll(groupID, "-", "") + "sa")
 
 	dstDir := filepath.Join(os.TempDir(), "e2e-aci-volume-"+composeAccountName)
 	srcDir := filepath.Join("..", "composefiles", "aci-demo")
@@ -663,28 +661,33 @@ func TestUpUpdate(t *testing.T) {
 	multiPortComposefile = filepath.Join(dstDir, multiPortComposefile)
 
 	volumeID := composeAccountName + "/" + fileshareName
-	t.Run("compose up", func(t *testing.T) {
-		const (
-			testFileName    = "msg.txt"
-			testFileContent = "VOLUME_OK"
-			projectName     = "acidemo"
-		)
-
-		c.RunDockerCmd("volume", "create", "--storage-account", composeAccountName, fileshareName)
+	const (
+		testFileName    = "msg.txt"
+		testFileContent = "VOLUME_OK"
+		projectName     = "acidemo"
+	)
+	var (
+		dnsLabelName = "nginx-" + groupID
+		fqdn         = dnsLabelName + "." + location + ".azurecontainer.io"
+	)
 
-		// Bootstrap volume
+	t.Run("compose up", func(t *testing.T) {
 		aciContext := store.AciContext{
 			SubscriptionID: sID,
 			Location:       location,
 			ResourceGroup:  groupID,
 		}
+		c.RunDockerCmd("compose", "up", "-f", singlePortVolumesComposefile, "--domainname", dnsLabelName, "--project-name", projectName)
+
+		// Volume should be autocreated by the "compose up"
 		uploadTestFile(t, aciContext, composeAccountName, fileshareName, testFileName, testFileContent)
+	})
 
-		dnsLabelName := "nginx-" + groupID
-		fqdn := dnsLabelName + "." + location + ".azurecontainer.io"
-		// Name of Compose project is taken from current folder "acie2e"
-		c.RunDockerCmd("compose", "up", "-f", singlePortVolumesComposefile, "--domainname", dnsLabelName, "--project-name", projectName)
+	t.Cleanup(func() {
+		c.RunDockerCmd("volume", "rm", volumeID)
+	})
 
+	t.Run("check deployed compose app", func(t *testing.T) {
 		res := c.RunDockerCmd("ps")
 		out := lines(res.Stdout())
 		// Check three containers are running
@@ -723,9 +726,6 @@ func TestUpUpdate(t *testing.T) {
 				composeAccountName, fileshareName, projectName),
 		})
 	})
-	t.Cleanup(func() {
-		c.RunDockerCmd("volume", "rm", volumeID)
-	})
 
 	t.Run("compose ps", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName, "--quiet")