Merge pull request #886 from docker/auto-create-aci-fileshare

Add auto creation of fileshares
This commit is contained in:
Guillaume Tardif 2020-11-23 09:31:57 +01:00 committed by GitHub
commit 8d7f213ba8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 36 deletions

View File

@ -28,6 +28,7 @@ import (
"github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/to" "github.com/Azure/go-autorest/autorest/to"
tm "github.com/buger/goterm" tm "github.com/buger/goterm"
"github.com/compose-spec/compose-go/types"
"github.com/gobwas/ws" "github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil" "github.com/gobwas/ws/wsutil"
"github.com/morikuni/aec" "github.com/morikuni/aec"
@ -35,6 +36,7 @@ import (
"github.com/docker/compose-cli/aci/convert" "github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/aci/login" "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/api/containers"
"github.com/docker/compose-cli/context/store" "github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/errdefs" "github.com/docker/compose-cli/errdefs"
@ -64,12 +66,46 @@ func createACIContainers(ctx context.Context, aciContext store.AciContext, group
return createOrUpdateACIContainers(ctx, aciContext, groupDefinition) 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 { func createOrUpdateACIContainers(ctx context.Context, aciContext store.AciContext, groupDefinition containerinstance.ContainerGroup) error {
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID) containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
if err != nil { if err != nil {
return errors.Wrapf(err, "cannot get container group client") return errors.Wrapf(err, "cannot get container group client")
} }
groupDisplay := "Group " + *groupDefinition.Name groupDisplay := "Group " + *groupDefinition.Name
w.Event(progress.Event{ w.Event(progress.Event{
ID: groupDisplay, ID: groupDisplay,

View File

@ -46,18 +46,49 @@ func newComposeService(ctx store.AciContext) aciComposeService {
func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error { func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error {
logrus.Debugf("Up on project with name %q", project.Name) logrus.Debugf("Up on project with name %q", project.Name)
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project, cs.storageLogin)
addTag(&groupDefinition, composeContainerTag)
if err := autocreateFileshares(ctx, project); err != nil {
return err
}
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project, cs.storageLogin)
if err != nil { if err != nil {
return err return err
} }
addTag(&groupDefinition, composeContainerTag)
return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition) 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
}
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 { func (cs *aciComposeService) Down(ctx context.Context, project string) error {
logrus.Debugf("Down on project with name %q", project) 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) cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
if err != nil { if err != nil {
return err return err

View File

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

View File

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

View File

@ -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) 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 errors.Wrapf(errdefs.ErrNotFound, "fileshare %q", fileshare)
} }
return err return err
@ -247,8 +247,11 @@ func (cs *aciVolumeService) Inspect(ctx context.Context, id string) (volumes.Vol
if err != nil { if err != nil {
return volumes.Volume{}, err 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 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 volumes.Volume{}, err
} }
return toVolume(storageAccount, fileshareName), nil return toVolume(storageAccount, fileshareName), nil

View File

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