Merge pull request #593 from docker/volume_create

ACI Volume create
This commit is contained in:
Guillaume Tardif 2020-09-10 15:32:12 +02:00 committed by GitHub
commit c7a456ab83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1142 additions and 549 deletions

View File

@ -129,6 +129,34 @@ func getACIContainerGroup(ctx context.Context, aciContext store.AciContext, cont
return containerGroupsClient.Get(ctx, aciContext.ResourceGroup, containerGroupName)
}
func getACIContainerGroups(ctx context.Context, subscriptionID string, resourceGroup string) ([]containerinstance.ContainerGroup, error) {
groupsClient, err := login.NewContainerGroupsClient(subscriptionID)
if err != nil {
return nil, err
}
var containerGroups []containerinstance.ContainerGroup
result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup)
if err != nil {
return nil, err
}
for result.NotDone() {
containerGroups = append(containerGroups, result.Values()...)
if err := result.NextWithContext(ctx); err != nil {
return nil, err
}
}
var groups []containerinstance.ContainerGroup
for _, group := range containerGroups {
group, err := groupsClient.Get(ctx, resourceGroup, *group.Name)
if err != nil {
return nil, err
}
groups = append(groups, group)
}
return groups, nil
}
func deleteACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) (containerinstance.ContainerGroup, error) {
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
if err != nil {

View File

@ -18,35 +18,29 @@ package aci
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/compose-cli/aci/convert"
"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/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"
"github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/errdefs"
)
const (
backendType = store.AciContextType
singleContainerTag = "docker-single-container"
composeContainerTag = "docker-compose-application"
dockerVolumeTag = "docker-volume"
composeContainerSeparator = "_"
)
@ -109,12 +103,16 @@ func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
aciComposeService: &aciComposeService{
ctx: aciCtx,
},
aciVolumeService: &aciVolumeService{
aciContext: aciCtx,
},
}
}
type aciAPIService struct {
*aciContainerService
*aciComposeService
*aciVolumeService
}
func (a *aciAPIService) ContainerService() containers.Service {
@ -129,58 +127,8 @@ func (a *aciAPIService) SecretsService() secrets.Service {
return nil
}
type aciContainerService struct {
ctx store.AciContext
}
func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) {
containerGroups, err := getContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
if err != nil {
return []containers.Container{}, err
}
var res []containers.Container
for _, group := range containerGroups {
if group.Containers == nil || len(*group.Containers) < 1 {
return []containers.Container{}, fmt.Errorf("no containers found in ACI container group %s", *group.Name)
}
for _, container := range *group.Containers {
if isContainerVisible(container, group, all) {
continue
}
c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container)
res = append(res, c)
}
}
return res, nil
}
func getContainerGroups(ctx context.Context, subscriptionID string, resourceGroup string) ([]containerinstance.ContainerGroup, error) {
groupsClient, err := login.NewContainerGroupsClient(subscriptionID)
if err != nil {
return nil, err
}
var containerGroups []containerinstance.ContainerGroup
result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup)
if err != nil {
return []containerinstance.ContainerGroup{}, err
}
for result.NotDone() {
containerGroups = append(containerGroups, result.Values()...)
if err := result.NextWithContext(ctx); err != nil {
return []containerinstance.ContainerGroup{}, err
}
}
var groups []containerinstance.ContainerGroup
for _, group := range containerGroups {
group, err := groupsClient.Get(ctx, resourceGroup, *group.Name)
if err != nil {
return []containerinstance.ContainerGroup{}, err
}
groups = append(groups, group)
}
return groups, nil
func (a *aciAPIService) VolumeService() volumes.Service {
return a.aciVolumeService
}
func getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string {
@ -195,26 +143,6 @@ func isContainerVisible(container containerinstance.Container, group containerin
return *container.Name == convert.ComposeDNSSidecarName || (!showAll && convert.GetStatus(container, group) != convert.StatusRunning)
}
func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
if strings.Contains(r.ID, composeContainerSeparator) {
return errors.New(fmt.Sprintf("invalid container name. ACI container name cannot include %q", composeContainerSeparator))
}
project, err := convert.ContainerToComposeProject(r)
if err != nil {
return err
}
logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID)
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project)
if err != nil {
return err
}
addTag(&groupDefinition, singleContainerTag)
return createACIContainers(ctx, cs.ctx, groupDefinition)
}
func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
if groupDefinition.Tags == nil {
groupDefinition.Tags = make(map[string]*string, 1)
@ -222,53 +150,6 @@ func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
groupDefinition.Tags[tagName] = to.StringPtr(tagName)
}
func (cs *aciContainerService) Start(ctx context.Context, containerID string) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s"
return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
}
containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return err
}
future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName)
if err != nil {
var aerr autorest.DetailedError
if ok := errors.As(err, &aerr); ok {
if aerr.StatusCode == http.StatusNotFound {
return errdefs.ErrNotFound
}
}
return err
}
return future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
}
func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error {
if timeout != nil && *timeout != uint32(0) {
return errors.Errorf("ACI integration does not support setting a timeout to stop a container before killing it.")
}
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s"
return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
}
return stopACIContainerGroup(ctx, cs.ctx, groupName)
}
func (cs *aciContainerService) Kill(ctx context.Context, containerID string, _ string) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot kill service %q from compose application %q, you can kill the entire compose app with docker kill %s"
return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
}
return stopACIContainerGroup(ctx, cs.ctx, groupName) // As ACI doesn't have a kill command, we are using the stop implementation instead
}
func getGroupAndContainerName(containerID string) (string, string) {
tokens := strings.Split(containerID, composeContainerSeparator)
groupName := tokens[0]
@ -279,244 +160,3 @@ func getGroupAndContainerName(containerID string) (string, string) {
}
return groupName, containerName
}
func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
err := verifyExecCommand(request.Command)
if err != nil {
return err
}
groupName, containerAciName := getGroupAndContainerName(name)
containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
if err != nil {
return err
}
return exec(
context.Background(),
*containerExecResponse.WebSocketURI,
*containerExecResponse.Password,
request,
)
}
func verifyExecCommand(command string) error {
tokens := strings.Split(command, " ")
if len(tokens) > 1 {
return errors.New("ACI exec command does not accept arguments to the command. " +
"Only the binary should be specified")
}
return nil
}
func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
groupName, containerAciName := getGroupAndContainerName(containerName)
var tail *int32
if req.Follow {
return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
}
if req.Tail != "all" {
reqTail, err := strconv.Atoi(req.Tail)
if err != nil {
return err
}
i32 := int32(reqTail)
tail = &i32
}
logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
if err != nil {
return err
}
_, err = fmt.Fprint(req.Writer, logs)
return err
}
func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
}
if !request.Force {
containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return err
}
cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName)
if err != nil {
if cg.StatusCode == http.StatusNotFound {
return errdefs.ErrNotFound
}
return err
}
for _, container := range *cg.Containers {
status := convert.GetStatus(container, cg)
if status == convert.StatusRunning {
return errdefs.ErrForbidden
}
}
}
cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
// Delete returns `StatusNoContent` if the group is not found
if cg.StatusCode == http.StatusNoContent {
return errdefs.ErrNotFound
}
if err != nil {
return err
}
return err
}
func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
groupName, containerName := getGroupAndContainerName(containerID)
cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
if err != nil {
return containers.Container{}, err
}
if cg.StatusCode == http.StatusNoContent {
return containers.Container{}, errdefs.ErrNotFound
}
var cc containerinstance.Container
var found = false
for _, c := range *cg.Containers {
if to.String(c.Name) == containerName {
cc = c
found = true
break
}
}
if !found {
return containers.Container{}, errdefs.ErrNotFound
}
return convert.ContainerGroupToContainer(containerID, cg, cc), nil
}
type aciComposeService struct {
ctx store.AciContext
}
func (cs *aciComposeService) Up(ctx context.Context, project *types.Project) error {
logrus.Debugf("Up on project with name %q\n", project.Name)
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project)
addTag(&groupDefinition, composeContainerTag)
if err != nil {
return err
}
return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
}
func (cs *aciComposeService) Down(ctx context.Context, project string) error {
logrus.Debugf("Down on project with name %q\n", project)
cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
if err != nil {
return err
}
if cg.StatusCode == http.StatusNoContent {
return errdefs.ErrNotFound
}
return err
}
func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return nil, err
}
group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, project)
if err != nil {
return []compose.ServiceStatus{}, err
}
if group.Containers == nil || len(*group.Containers) < 1 {
return []compose.ServiceStatus{}, fmt.Errorf("no containers found in ACI container group %s", project)
}
res := []compose.ServiceStatus{}
for _, container := range *group.Containers {
if isContainerVisible(container, group, false) {
continue
}
res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container))
}
return res, nil
}
func (cs *aciComposeService) List(ctx context.Context, project string) ([]compose.Stack, error) {
containerGroups, err := getContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
if err != nil {
return []compose.Stack{}, err
}
stacks := []compose.Stack{}
for _, group := range containerGroups {
if _, found := group.Tags[composeContainerTag]; !found {
continue
}
if project != "" && *group.Name != project {
continue
}
state := compose.RUNNING
for _, container := range *group.ContainerGroupProperties.Containers {
containerState := convert.GetStatus(container, group)
if containerState != compose.RUNNING {
state = containerState
break
}
}
stacks = append(stacks, compose.Stack{
ID: *group.ID,
Name: *group.Name,
Status: state,
})
}
return stacks, nil
}
func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writer) error {
return errdefs.ErrNotImplemented
}
func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
return nil, errdefs.ErrNotImplemented
}
type aciCloudService struct {
loginService login.AzureLoginServiceAPI
}
func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
opts, ok := params.(LoginParams)
if !ok {
return errors.New("Could not read azure LoginParams struct from generic parameter")
}
if opts.ClientID != "" {
return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID)
}
return cs.loginService.Login(ctx, opts.TenantID)
}
func (cs *aciCloudService) Logout(ctx context.Context) error {
return cs.loginService.Logout(ctx)
}
func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
contextHelper := newContextCreateHelper()
createOpts := params.(ContextParams)
return contextHelper.createContextData(ctx, createOpts)
}

50
aci/cloud.go Normal file
View File

@ -0,0 +1,50 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aci
import (
"context"
"github.com/pkg/errors"
"github.com/docker/compose-cli/aci/login"
)
type aciCloudService struct {
loginService login.AzureLoginServiceAPI
}
func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
opts, ok := params.(LoginParams)
if !ok {
return errors.New("could not read Azure LoginParams struct from generic parameter")
}
if opts.ClientID != "" {
return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID)
}
return cs.loginService.Login(ctx, opts.TenantID)
}
func (cs *aciCloudService) Logout(ctx context.Context) error {
return cs.loginService.Logout(ctx)
}
func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
contextHelper := newContextCreateHelper()
createOpts := params.(ContextParams)
return contextHelper.createContextData(ctx, createOpts)
}

126
aci/compose.go Normal file
View File

@ -0,0 +1,126 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aci
import (
"context"
"fmt"
"io"
"net/http"
"github.com/compose-spec/compose-go/types"
"github.com/sirupsen/logrus"
"github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/errdefs"
)
type aciComposeService struct {
ctx store.AciContext
}
func (cs *aciComposeService) Up(ctx context.Context, project *types.Project) error {
logrus.Debugf("Up on project with name %q", project.Name)
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project)
addTag(&groupDefinition, composeContainerTag)
if err != nil {
return err
}
return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
}
func (cs *aciComposeService) Down(ctx context.Context, project string) error {
logrus.Debugf("Down on project with name %q", project)
cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
if err != nil {
return err
}
if cg.StatusCode == http.StatusNoContent {
return errdefs.ErrNotFound
}
return err
}
func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return nil, err
}
group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, project)
if err != nil {
return nil, err
}
if group.Containers == nil || len(*group.Containers) == 0 {
return nil, fmt.Errorf("no containers found in ACI container group %s", project)
}
res := []compose.ServiceStatus{}
for _, container := range *group.Containers {
if isContainerVisible(container, group, false) {
continue
}
res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container))
}
return res, nil
}
func (cs *aciComposeService) List(ctx context.Context, project string) ([]compose.Stack, error) {
containerGroups, err := getACIContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
if err != nil {
return nil, err
}
stacks := []compose.Stack{}
for _, group := range containerGroups {
if _, found := group.Tags[composeContainerTag]; !found {
continue
}
if project != "" && *group.Name != project {
continue
}
state := compose.RUNNING
for _, container := range *group.ContainerGroupProperties.Containers {
containerState := convert.GetStatus(container, group)
if containerState != compose.RUNNING {
state = containerState
break
}
}
stacks = append(stacks, compose.Stack{
ID: *group.ID,
Name: *group.Name,
Status: state,
})
}
return stacks, nil
}
func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writer) error {
return errdefs.ErrNotImplemented
}
func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
return nil, errdefs.ErrNotImplemented
}

253
aci/containers.go Normal file
View File

@ -0,0 +1,253 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aci
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/to"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/errdefs"
)
type aciContainerService struct {
ctx store.AciContext
}
func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) {
containerGroups, err := getACIContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
if err != nil {
return nil, err
}
var res []containers.Container
for _, group := range containerGroups {
if group.Containers == nil || len(*group.Containers) == 0 {
return nil, fmt.Errorf("no containers found in ACI container group %s", *group.Name)
}
for _, container := range *group.Containers {
if isContainerVisible(container, group, all) {
continue
}
c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container)
res = append(res, c)
}
}
return res, nil
}
func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
if strings.Contains(r.ID, composeContainerSeparator) {
return fmt.Errorf("invalid container name. ACI container name cannot include %q", composeContainerSeparator)
}
project, err := convert.ContainerToComposeProject(r)
if err != nil {
return err
}
logrus.Debugf("Running container %q with name %q", r.Image, r.ID)
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project)
if err != nil {
return err
}
addTag(&groupDefinition, singleContainerTag)
return createACIContainers(ctx, cs.ctx, groupDefinition)
}
func (cs *aciContainerService) Start(ctx context.Context, containerID string) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return err
}
future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName)
if err != nil {
var aerr autorest.DetailedError
if ok := errors.As(err, &aerr); ok {
if aerr.StatusCode == http.StatusNotFound {
return errdefs.ErrNotFound
}
}
return err
}
return future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
}
func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error {
if timeout != nil && *timeout != uint32(0) {
return fmt.Errorf("the ACI integration does not support setting a timeout to stop a container before killing it")
}
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
return stopACIContainerGroup(ctx, cs.ctx, groupName)
}
func (cs *aciContainerService) Kill(ctx context.Context, containerID string, _ string) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot kill service %q from compose application %q, you can kill the entire compose app with docker kill %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
return stopACIContainerGroup(ctx, cs.ctx, groupName) // As ACI doesn't have a kill command, we are using the stop implementation instead
}
func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
err := verifyExecCommand(request.Command)
if err != nil {
return err
}
groupName, containerAciName := getGroupAndContainerName(name)
containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
if err != nil {
return err
}
return exec(
context.Background(),
*containerExecResponse.WebSocketURI,
*containerExecResponse.Password,
request,
)
}
func verifyExecCommand(command string) error {
tokens := strings.Split(command, " ")
if len(tokens) > 1 {
return errors.New("ACI exec command does not accept arguments to the command. " +
"Only the binary should be specified")
}
return nil
}
func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
groupName, containerAciName := getGroupAndContainerName(containerName)
var tail *int32
if req.Follow {
return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
}
if req.Tail != "all" {
reqTail, err := strconv.Atoi(req.Tail)
if err != nil {
return err
}
i32 := int32(reqTail)
tail = &i32
}
logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
if err != nil {
return err
}
_, err = fmt.Fprint(req.Writer, logs)
return err
}
func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
if !request.Force {
containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return err
}
cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName)
if err != nil {
if cg.StatusCode == http.StatusNotFound {
return errdefs.ErrNotFound
}
return err
}
for _, container := range *cg.Containers {
status := convert.GetStatus(container, cg)
if status == convert.StatusRunning {
return errdefs.ErrForbidden
}
}
}
cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
// Delete returns `StatusNoContent` if the group is not found
if cg.StatusCode == http.StatusNoContent {
return errdefs.ErrNotFound
}
if err != nil {
return err
}
return err
}
func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
groupName, containerName := getGroupAndContainerName(containerID)
cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
if err != nil {
return containers.Container{}, err
}
if cg.StatusCode == http.StatusNoContent {
return containers.Container{}, errdefs.ErrNotFound
}
var cc containerinstance.Container
var found = false
for _, c := range *cg.Containers {
if to.String(c.Name) == containerName {
cc = c
found = true
break
}
}
if !found {
return containers.Container{}, errdefs.ErrNotFound
}
return convert.ContainerGroupToContainer(containerID, cg, cc), nil
}

View File

@ -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,
storageHelper := login.StorageLogin{
AciContext: aciContext,
}
volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
if err != nil {
@ -205,7 +200,7 @@ func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, err
return secretVolumes, nil
}
func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageAccountHelper) (map[string]bool, []containerinstance.Volume, error) {
func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageLogin) (map[string]bool, []containerinstance.Volume, error) {
azureFileVolumesMap := make(map[string]bool, len(p.Volumes))
var azureFileVolumesSlice []containerinstance.Volume
for name, v := range p.Volumes {

View File

@ -67,6 +67,19 @@ func NewStorageAccountsClient(subscriptionID string) (storage.AccountsClient, er
return containerGroupsClient, nil
}
// NewFileShareClient get client to manipulate file shares
func NewFileShareClient(subscriptionID string) (storage.FileSharesClient, error) {
containerGroupsClient := storage.NewFileSharesClient(subscriptionID)
err := setupClient(&containerGroupsClient.Client)
if err != nil {
return storage.FileSharesClient{}, err
}
containerGroupsClient.PollingDelay = 5 * time.Second
containerGroupsClient.RetryAttempts = 30
containerGroupsClient.RetryDuration = 1 * time.Second
return containerGroupsClient, nil
}
// NewSubscriptionsClient get subscription client
func NewSubscriptionsClient() (subscription.SubscriptionsClient, error) {
subc := subscription.NewSubscriptionsClient()

View File

@ -25,14 +25,13 @@ import (
"github.com/docker/compose-cli/context/store"
)
// StorageAccountHelper helper for Azure Storage Account
type StorageAccountHelper struct {
LoginService AzureLoginService
AciContext store.AciContext
// StorageLogin helper for Azure Storage Login
type StorageLogin struct {
AciContext store.AciContext
}
// GetAzureStorageAccountKey retrieves the storage account ket from the current azure login
func (helper StorageAccountHelper) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) {
func (helper StorageLogin) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) {
client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID)
if err != nil {
return "", err

234
aci/volumes.go Normal file
View File

@ -0,0 +1,234 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aci
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
"github.com/Azure/go-autorest/autorest/to"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/errdefs"
"github.com/docker/compose-cli/progress"
)
type aciVolumeService struct {
aciContext store.AciContext
}
func (cs *aciVolumeService) List(ctx context.Context) ([]volumes.Volume, error) {
accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
if err != nil {
return nil, err
}
result, err := accountClient.ListByResourceGroup(ctx, cs.aciContext.ResourceGroup)
if err != nil {
return nil, err
}
accounts := result.Value
fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
if err != nil {
return nil, err
}
fileShares := []volumes.Volume{}
for _, account := range *accounts {
fileSharePage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, *account.Name, "", "", "")
if err != nil {
return nil, err
}
for fileSharePage.NotDone() {
values := fileSharePage.Values()
for _, fileShare := range values {
fileShares = append(fileShares, toVolume(account, *fileShare.Name))
}
if err := fileSharePage.NextWithContext(ctx); err != nil {
return nil, err
}
}
}
return fileShares, nil
}
// 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) {
opts, ok := options.(VolumeCreateOptions)
if !ok {
return volumes.Volume{}, errors.New("could not read Azure VolumeCreateOptions struct from generic parameter")
}
w := progress.ContextWriter(ctx)
w.Event(event(opts.Account, progress.Working, "Validating"))
accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
if err != nil {
return volumes.Volume{}, err
}
account, err := accountClient.GetProperties(ctx, cs.aciContext.ResourceGroup, opts.Account, "")
if err == nil {
w.Event(event(opts.Account, progress.Done, "Use existing"))
} else {
if account.StatusCode != http.StatusNotFound {
return volumes.Volume{}, err
}
result, err := accountClient.CheckNameAvailability(ctx, storage.AccountCheckNameAvailabilityParameters{
Name: to.StringPtr(opts.Account),
Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
})
if err != nil {
return volumes.Volume{}, err
}
if !*result.NameAvailable {
return volumes.Volume{}, errors.New("error: " + *result.Message)
}
parameters := defaultStorageAccountParams(cs.aciContext)
w.Event(event(opts.Account, progress.Working, "Creating"))
future, err := accountClient.Create(ctx, cs.aciContext.ResourceGroup, opts.Account, parameters)
if err != nil {
w.Event(errorEvent(opts.Account))
return volumes.Volume{}, err
}
if err := future.WaitForCompletionRef(ctx, accountClient.Client); err != nil {
w.Event(errorEvent(opts.Account))
return volumes.Volume{}, err
}
account, err = future.Result(accountClient)
if err != nil {
w.Event(errorEvent(opts.Account))
return volumes.Volume{}, err
}
w.Event(event(opts.Account, progress.Done, "Created"))
}
w.Event(event(opts.Fileshare, progress.Working, "Creating"))
fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
if err != nil {
return volumes.Volume{}, err
}
fileShare, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, "")
if err == nil {
w.Event(errorEvent(opts.Fileshare))
return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", opts.Fileshare)
}
if fileShare.StatusCode != http.StatusNotFound {
w.Event(errorEvent(opts.Fileshare))
return volumes.Volume{}, err
}
fileShare, err = fileShareClient.Create(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, storage.FileShare{})
if err != nil {
w.Event(errorEvent(opts.Fileshare))
return volumes.Volume{}, err
}
w.Event(event(opts.Fileshare, progress.Done, "Created"))
return toVolume(account, *fileShare.Name), nil
}
func event(resource string, status progress.EventStatus, text string) progress.Event {
return progress.Event{
ID: resource,
Status: status,
StatusText: text,
}
}
func errorEvent(resource string) progress.Event {
return progress.Event{
ID: resource,
Status: progress.Error,
StatusText: "Error",
}
}
func (cs *aciVolumeService) Delete(ctx context.Context, id string, options interface{}) error {
tokens := strings.Split(id, "@")
if len(tokens) != 2 {
return errors.New("wrong format for volume ID : should be storageaccount@fileshare")
}
storageAccount := tokens[0]
fileshare := tokens[1]
fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
if err != nil {
return err
}
fileShareItemsPage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, storageAccount, "", "", "")
if err != nil {
return err
}
fileshares := fileShareItemsPage.Values()
if len(fileshares) == 1 && *fileshares[0].Name == fileshare {
storageAccountsClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
if err != nil {
return err
}
account, err := storageAccountsClient.GetProperties(ctx, cs.aciContext.ResourceGroup, storageAccount, "")
if err != nil {
return err
}
if err == nil {
if _, ok := account.Tags[dockerVolumeTag]; ok {
result, err := storageAccountsClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount)
if result.StatusCode == http.StatusNoContent {
return errors.Wrapf(errdefs.ErrNotFound, "storage account %s does not exist", storageAccount)
}
return err
}
}
}
result, err := fileShareClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshare)
if result.StatusCode == 204 {
return errors.Wrapf(errdefs.ErrNotFound, "fileshare %q", fileshare)
}
return err
}
func toVolume(account storage.Account, fileShareName string) volumes.Volume {
return volumes.Volume{
ID: VolumeID(*account.Name, fileShareName),
Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name),
}
}
// VolumeID generate volume ID from azure storage accoun & fileshare
func VolumeID(storageAccount string, fileShareName string) string {
return fmt.Sprintf("%s@%s", storageAccount, fileShareName)
}
func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters {
tags := map[string]*string{dockerVolumeTag: to.StringPtr(dockerVolumeTag)}
return storage.AccountCreateParameters{
Location: to.StringPtr(aciContext.Location),
Sku: &storage.Sku{
Name: storage.StandardLRS,
},
Tags: tags,
}
}

View File

@ -22,6 +22,7 @@ import (
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/containers"
"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"
@ -86,3 +87,12 @@ 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 {
return vs
}
return &volumeService{}
}

39
api/client/volume.go Normal file
View File

@ -0,0 +1,39 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/errdefs"
)
type volumeService struct {
}
func (c *volumeService) List(ctx context.Context) ([]volumes.Volume, error) {
return nil, errdefs.ErrNotImplemented
}
func (c *volumeService) Create(ctx context.Context, options interface{}) (volumes.Volume, error) {
return volumes.Volume{}, errdefs.ErrNotImplemented
}
func (c *volumeService) Delete(ctx context.Context, id string, options interface{}) error {
return errdefs.ErrNotImplemented
}

37
api/volumes/api.go Normal file
View File

@ -0,0 +1,37 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package volumes
import (
"context"
)
// Volume volume info
type Volume struct {
ID string
Description string
}
// Service interacts with the underlying container backend
type Service interface {
// List returns all available volumes
List(ctx context.Context) ([]Volume, error)
// Create creates a new volume
Create(ctx context.Context, options interface{}) (Volume, error)
// Delete deletes an existing volume
Delete(ctx context.Context, volumeID string, options interface{}) error
}

View File

@ -26,6 +26,7 @@ import (
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/secrets"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/context/cloud"
"github.com/docker/compose-cli/errdefs"
)
@ -53,8 +54,9 @@ var backends = struct {
// Service aggregates the service interfaces
type Service interface {
ContainerService() containers.Service
SecretsService() secrets.Service
ComposeService() compose.Service
SecretsService() secrets.Service
VolumeService() volumes.Service
}
// Register adds a typed backend to the registry

115
cli/cmd/volume/acivolume.go Normal file
View File

@ -0,0 +1,115 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package volume
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"github.com/docker/compose-cli/aci"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/progress"
)
// ACICommand manage volumes
func ACICommand() *cobra.Command {
cmd := &cobra.Command{
Use: "volume",
Short: "Manages volumes",
}
cmd.AddCommand(
createVolume(),
listVolume(),
rmVolume(),
)
return cmd
}
func createVolume() *cobra.Command {
aciOpts := aci.VolumeCreateOptions{}
cmd := &cobra.Command{
Use: "create --storage-account ACCOUNT --fileshare FILESHARE",
Short: "Creates an Azure file share to use as ACI volume.",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
c, err := client.New(ctx)
if err != nil {
return err
}
err = progress.Run(ctx, func(ctx context.Context) error {
if _, err := c.VolumeService().Create(ctx, aciOpts); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
fmt.Println(aci.VolumeID(aciOpts.Account, aciOpts.Fileshare))
return nil
},
}
cmd.Flags().StringVar(&aciOpts.Account, "storage-account", "", "Storage account name")
cmd.Flags().StringVar(&aciOpts.Fileshare, "fileshare", "", "Fileshare name")
_ = cmd.MarkFlagRequired("fileshare")
_ = cmd.MarkFlagRequired("storage-account")
return cmd
}
func rmVolume() *cobra.Command {
cmd := &cobra.Command{
Use: "rm [OPTIONS] VOLUME [VOLUME...]",
Short: "Remove one or more volumes.",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := client.New(cmd.Context())
if err != nil {
return err
}
var errs *multierror.Error
for _, id := range args {
err = c.VolumeService().Delete(cmd.Context(), id, nil)
if err != nil {
errs = multierror.Append(errs, err)
continue
}
fmt.Println(id)
}
if errs != nil {
errs.ErrorFormat = formatErrors
}
return errs.ErrorOrNil()
},
}
return cmd
}
func formatErrors(errs []error) string {
messages := make([]string, len(errs))
for i, err := range errs {
messages[i] = "Error: " + err.Error()
}
return strings.Join(messages, "\n")
}

66
cli/cmd/volume/list.go Normal file
View File

@ -0,0 +1,66 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package volume
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/volumes"
)
func listVolume() *cobra.Command {
cmd := &cobra.Command{
Use: "ls",
Short: "list available volumes in context.",
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\n", vol.ID, vol.Description)
}
}, "ID", "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"))
printer(w)
_ = w.Flush()
}

View File

@ -0,0 +1,38 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package volume
import (
"bytes"
"testing"
"gotest.tools/v3/golden"
"github.com/docker/compose-cli/api/volumes"
)
func TestPrintList(t *testing.T) {
secrets := []volumes.Volume{
{
ID: "volume@123",
Description: "volume 123",
},
}
out := &bytes.Buffer{}
printList(out, secrets)
golden.Assert(t, out.String(), "volumes-out.golden")
}

View File

@ -0,0 +1,2 @@
ID DESCRIPTION
volume@123 volume 123

View File

@ -28,23 +28,15 @@ import (
"time"
"github.com/docker/compose-cli/cli/cmd/compose"
"github.com/docker/compose-cli/cli/cmd/logout"
volume "github.com/docker/compose-cli/cli/cmd/volume"
"github.com/docker/compose-cli/errdefs"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
// Backend registrations
_ "github.com/docker/compose-cli/aci"
_ "github.com/docker/compose-cli/ecs"
_ "github.com/docker/compose-cli/ecs/local"
_ "github.com/docker/compose-cli/example"
_ "github.com/docker/compose-cli/local"
"github.com/docker/compose-cli/metrics"
"github.com/docker/compose-cli/cli/cmd"
contextcmd "github.com/docker/compose-cli/cli/cmd/context"
"github.com/docker/compose-cli/cli/cmd/login"
@ -54,6 +46,11 @@ import (
"github.com/docker/compose-cli/config"
apicontext "github.com/docker/compose-cli/context"
"github.com/docker/compose-cli/context/store"
_ "github.com/docker/compose-cli/ecs"
_ "github.com/docker/compose-cli/ecs/local"
_ "github.com/docker/compose-cli/example"
_ "github.com/docker/compose-cli/local"
"github.com/docker/compose-cli/metrics"
)
var (
@ -182,6 +179,11 @@ func main() {
ctype = cc.Type()
}
if ctype == store.AciContextType {
// we can also pass ctype as a parameter to the volume command and customize subcommands, flags, etc. when we have other backend implementations
root.AddCommand(volume.ACICommand())
}
metrics.Track(ctype, os.Args[1:], root.PersistentFlags())
ctx = apicontext.WithCurrentContext(ctx, currentContext)

View File

@ -21,10 +21,10 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/containers"
"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"
@ -97,6 +97,10 @@ func (a *ecsAPIService) SecretsService() secrets.Service {
return a
}
func (a *ecsAPIService) VolumeService() volumes.Service {
return nil
}
func getCloudService() (cloud.Service, error) {
return ecsCloudService{}, nil
}

View File

@ -22,11 +22,11 @@ import (
"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/api/volumes"
"github.com/docker/compose-cli/backend"
"github.com/docker/compose-cli/context/cloud"
"github.com/docker/compose-cli/context/store"
"github.com/docker/docker/client"
)
const backendType = store.EcsLocalSimulationContextType
@ -58,6 +58,10 @@ func (e ecsLocalSimulation) ContainerService() containers.Service {
return nil
}
func (e ecsLocalSimulation) VolumeService() volumes.Service {
return nil
}
func (e ecsLocalSimulation) SecretsService() secrets.Service {
return nil
}

View File

@ -29,6 +29,7 @@ import (
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/containers"
"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"
@ -51,6 +52,10 @@ func (a *apiService) SecretsService() secrets.Service {
return nil
}
func (a *apiService) VolumeService() volumes.Service {
return nil
}
func init() {
backend.Register("example", "example", service, cloud.NotImplementedCloudService)
}

View File

@ -39,6 +39,7 @@ import (
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/containers"
"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"
@ -75,6 +76,10 @@ func (ms *local) SecretsService() secrets.Service {
return nil
}
func (ms *local) VolumeService() volumes.Service {
return nil
}
func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, error) {
c, err := ms.apiClient.ContainerInspect(ctx, id)
if err != nil {

View File

@ -39,7 +39,6 @@ import (
"gotest.tools/v3/poll"
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
azure_storage "github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/storage/mgmt/storage"
"github.com/Azure/azure-storage-file-go/azfile"
"github.com/Azure/go-autorest/autorest/to"
@ -48,7 +47,6 @@ import (
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/errdefs"
"github.com/docker/compose-cli/tests/aci-e2e/storage"
. "github.com/docker/compose-cli/tests/framework"
)
@ -131,7 +129,7 @@ func TestContainerRunVolume(t *testing.T) {
sID, rg := setupTestResourceGroup(t, c)
const (
testShareName = "dockertestshare"
fileshareName = "dockertestshare"
testFileContent = "Volume mounted successfully!"
testFileName = "index.html"
)
@ -142,31 +140,78 @@ func TestContainerRunVolume(t *testing.T) {
Location: location,
ResourceGroup: rg,
}
saName := "e2e" + strconv.Itoa(int(time.Now().UnixNano()))
_, cleanupSa := createStorageAccount(t, aciContext, saName)
t.Cleanup(func() {
if err := cleanupSa(); err != nil {
t.Error(err)
}
})
keys := getStorageKeys(t, aciContext, saName)
assert.Assert(t, len(keys) > 0)
k := *keys[0].Value
cred, u := createFileShare(t, k, testShareName, saName)
uploadFile(t, *cred, u.String(), testFileName, testFileContent)
// Used in subtests
var (
container string
hostIP string
endpoint string
container string
hostIP string
endpoint string
volumeID string
accountName = "e2e" + strconv.Itoa(int(time.Now().UnixNano()))
)
t.Run("check volume name validity", func(t *testing.T) {
invalidName := "some-storage-123"
res := c.RunDockerOrExitError("volume", "create", "--storage-account", invalidName, "--fileshare", fileshareName)
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "some-storage-123 is not a valid storage account name. Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only.",
})
})
t.Run("create volumes", func(t *testing.T) {
c.RunDockerCmd("volume", "create", "--storage-account", accountName, "--fileshare", fileshareName)
})
volumeID = accountName + "@" + fileshareName
t.Cleanup(func() {
c.RunDockerCmd("volume", "rm", volumeID)
res := c.RunDockerCmd("volume", "ls")
lines := lines(res.Stdout())
assert.Equal(t, len(lines), 1)
})
t.Run("create second fileshare", func(t *testing.T) {
c.RunDockerCmd("volume", "create", "--storage-account", accountName, "--fileshare", "dockertestshare2")
})
volumeID2 := accountName + "@dockertestshare2"
t.Run("list volumes", func(t *testing.T) {
res := c.RunDockerCmd("volume", "ls")
lines := lines(res.Stdout())
assert.Equal(t, len(lines), 3)
firstAccount := lines[1]
fields := strings.Fields(firstAccount)
assert.Equal(t, fields[0], volumeID)
secondAccount := lines[2]
fields = strings.Fields(secondAccount)
assert.Equal(t, fields[0], volumeID2)
})
t.Run("delete only fileshare", func(t *testing.T) {
c.RunDockerCmd("volume", "rm", volumeID2)
res := c.RunDockerCmd("volume", "ls")
lines := lines(res.Stdout())
assert.Equal(t, len(lines), 2)
assert.Assert(t, !strings.Contains(res.Stdout(), "dockertestshare2"), "second fileshare still visible after rm")
})
t.Run("upload file", func(t *testing.T) {
storageLogin := login.StorageLogin{AciContext: aciContext}
key, err := storageLogin.GetAzureStorageAccountKey(context.TODO(), accountName)
assert.NilError(t, err)
cred, err := azfile.NewSharedKeyCredential(accountName, key)
assert.NilError(t, err)
u, _ := url.Parse(fmt.Sprintf("https://%s.file.core.windows.net/%s", accountName, fileshareName))
uploadFile(t, *cred, u.String(), testFileName, testFileContent)
})
t.Run("run", func(t *testing.T) {
mountTarget := "/usr/share/nginx/html"
res := c.RunDockerCmd(
"run", "-d",
"-v", fmt.Sprintf("%s@%s:%s", saName, testShareName, mountTarget),
"-v", fmt.Sprintf("%s:%s", volumeID, mountTarget),
"-p", "80:80",
"nginx",
)
@ -189,7 +234,7 @@ func TestContainerRunVolume(t *testing.T) {
t.Run("ps", func(t *testing.T) {
res := c.RunDockerCmd("ps")
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
out := lines(res.Stdout())
l := out[len(out)-1]
assert.Assert(t, strings.Contains(l, container), "Looking for %q in line: %s", container, l)
assert.Assert(t, strings.Contains(l, "nginx"))
@ -284,6 +329,10 @@ func TestContainerRunVolume(t *testing.T) {
})
}
func lines(output string) []string {
return strings.Split(strings.TrimSpace(output), "\n")
}
func TestContainerRunAttached(t *testing.T) {
c := NewParallelE2eCLI(t, binDir)
_, _ = setupTestResourceGroup(t, c)
@ -374,11 +423,11 @@ func TestContainerRunAttached(t *testing.T) {
t.Run("ps stopped container with --all", func(t *testing.T) {
res := c.RunDockerCmd("ps", container)
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
out := lines(res.Stdout())
assert.Assert(t, is.Len(out, 1))
res = c.RunDockerCmd("ps", "--all", container)
out = strings.Split(strings.TrimSpace(res.Stdout()), "\n")
out = lines(res.Stdout())
assert.Assert(t, is.Len(out, 2))
})
@ -415,7 +464,7 @@ func TestComposeUpUpdate(t *testing.T) {
// Name of Compose project is taken from current folder "acie2e"
c.RunDockerCmd("compose", "up", "-f", composeFile)
res := c.RunDockerCmd("ps")
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
out := lines(res.Stdout())
// Check three containers are running
assert.Assert(t, is.Len(out, 4))
webRunning := false
@ -444,7 +493,7 @@ func TestComposeUpUpdate(t *testing.T) {
t.Run("compose ps", func(t *testing.T) {
res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName)
lines := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
lines := lines(res.Stdout())
assert.Assert(t, is.Len(lines, 4))
var wordsDisplayed, webDisplayed, dbDisplayed bool
for _, line := range lines {
@ -468,7 +517,7 @@ func TestComposeUpUpdate(t *testing.T) {
t.Run("compose ls", func(t *testing.T) {
res := c.RunDockerCmd("compose", "ls")
lines := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
lines := lines(res.Stdout())
assert.Equal(t, 2, len(lines))
fields := strings.Fields(lines[1])
@ -485,7 +534,7 @@ func TestComposeUpUpdate(t *testing.T) {
t.Run("update", func(t *testing.T) {
c.RunDockerCmd("compose", "up", "-f", composeFileMultiplePorts, "--project-name", composeProjectName)
res := c.RunDockerCmd("ps")
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
out := lines(res.Stdout())
// Check three containers are running
assert.Assert(t, is.Len(out, 4))
@ -527,7 +576,7 @@ func TestComposeUpUpdate(t *testing.T) {
t.Run("down", func(t *testing.T) {
c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
res := c.RunDockerCmd("ps")
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
out := lines(res.Stdout())
assert.Equal(t, len(out), 1)
})
}
@ -548,7 +597,7 @@ func TestRunEnvVars(t *testing.T) {
cmd.Env = append(cmd.Env, "MYSQL_USER=user1")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Success)
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
out := lines(res.Stdout())
container := strings.TrimSpace(out[len(out)-1])
res = c.RunDockerCmd("inspect", container)
@ -583,7 +632,7 @@ func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string) {
createAciContextAndUseIt(t, c, sID, rg)
// Check nothing is running
res := c.RunDockerCmd("ps")
assert.Assert(t, is.Len(strings.Split(strings.TrimSpace(res.Stdout()), "\n"), 1))
assert.Assert(t, is.Len(lines(res.Stdout()), 1))
return sID, rg
}
@ -637,37 +686,6 @@ func createAciContextAndUseIt(t *testing.T, c *E2eCLI, sID, rgName string) {
res.Assert(t, icmd.Expected{Out: contextName + " *"})
}
func createStorageAccount(t *testing.T, aciContext store.AciContext, name string) (azure_storage.Account, func() error) {
account, err := storage.CreateStorageAccount(context.TODO(), aciContext, name)
assert.Check(t, is.Nil(err))
assert.Check(t, is.Equal(*(account.Name), name))
return account, func() error { return deleteStorageAccount(aciContext, name) }
}
func deleteStorageAccount(aciContext store.AciContext, name string) error {
_, err := storage.DeleteStorageAccount(context.TODO(), aciContext, name)
return err
}
func getStorageKeys(t *testing.T, aciContext store.AciContext, saName string) []azure_storage.AccountKey {
l, err := storage.ListKeys(context.TODO(), aciContext, saName)
assert.NilError(t, err)
assert.Assert(t, l.Keys != nil)
return *l.Keys
}
func createFileShare(t *testing.T, key, share, storageAccount string) (*azfile.SharedKeyCredential, *url.URL) {
// Create a ShareURL object that wraps a soon-to-be-created share's URL and a default pipeline.
u, _ := url.Parse(fmt.Sprintf("https://%s.file.core.windows.net/%s", storageAccount, share))
cred, err := azfile.NewSharedKeyCredential(storageAccount, key)
assert.NilError(t, err)
shareURL := azfile.NewShareURL(*u, azfile.NewPipeline(cred, azfile.PipelineOptions{}))
_, err = shareURL.Create(context.TODO(), azfile.Metadata{}, 0)
assert.NilError(t, err)
return cred, u
}
func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName, content string) {
fURL, err := url.Parse(baseURL + "/" + fileName)
assert.NilError(t, err)
@ -677,7 +695,7 @@ func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName
}
func getContainerName(stdout string) string {
out := strings.Split(strings.TrimSpace(stdout), "\n")
out := lines(stdout)
return strings.TrimSpace(out[len(out)-1])
}

View File

@ -1,93 +0,0 @@
/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package storage
import (
"context"
"errors"
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/storage/mgmt/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/to"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/context/store"
)
// CreateStorageAccount creates a new storage account.
func CreateStorageAccount(ctx context.Context, aciContext store.AciContext, accountName string) (storage.Account, error) {
storageAccountsClient := getStorageAccountsClient(aciContext)
result, err := storageAccountsClient.CheckNameAvailability(
ctx,
storage.AccountCheckNameAvailabilityParameters{
Name: to.StringPtr(accountName),
Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
})
if err != nil {
return storage.Account{}, err
}
if !*result.NameAvailable {
return storage.Account{}, errors.New("storage account name already exists" + accountName)
}
future, err := storageAccountsClient.Create(
ctx,
aciContext.ResourceGroup,
accountName,
storage.AccountCreateParameters{
Sku: &storage.Sku{
Name: storage.StandardLRS,
},
Location: to.StringPtr(aciContext.Location),
AccountPropertiesCreateParameters: &storage.AccountPropertiesCreateParameters{}})
if err != nil {
return storage.Account{}, err
}
err = future.WaitForCompletionRef(ctx, storageAccountsClient.Client)
if err != nil {
return storage.Account{}, err
}
return future.Result(storageAccountsClient)
}
// DeleteStorageAccount deletes a given storage account
func DeleteStorageAccount(ctx context.Context, aciContext store.AciContext, accountName string) (autorest.Response, error) {
storageAccountsClient := getStorageAccountsClient(aciContext)
response, err := storageAccountsClient.Delete(ctx, aciContext.ResourceGroup, accountName)
if err != nil {
return autorest.Response{}, err
}
return response, err
}
// ListKeys lists the storage account keys
func ListKeys(ctx context.Context, aciContext store.AciContext, accountName string) (storage.AccountListKeysResult, error) {
storageAccountsClient := getStorageAccountsClient(aciContext)
keys, err := storageAccountsClient.ListKeys(ctx, aciContext.ResourceGroup, accountName)
if err != nil {
return storage.AccountListKeysResult{}, err
}
return keys, nil
}
func getStorageAccountsClient(aciContext store.AciContext) storage.AccountsClient {
storageAccountsClient := storage.NewAccountsClient(aciContext.SubscriptionID)
autho, _ := login.NewAuthorizerFromLogin()
storageAccountsClient.Authorizer = autho
return storageAccountsClient
}

View File

@ -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 (