mirror of
https://github.com/docker/compose.git
synced 2025-07-25 22:54:54 +02:00
commit
c7a456ab83
28
aci/aci.go
28
aci/aci.go
@ -129,6 +129,34 @@ func getACIContainerGroup(ctx context.Context, aciContext store.AciContext, cont
|
|||||||
return containerGroupsClient.Get(ctx, aciContext.ResourceGroup, containerGroupName)
|
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) {
|
func deleteACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) (containerinstance.ContainerGroup, error) {
|
||||||
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
|
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
376
aci/backend.go
376
aci/backend.go
@ -18,35 +18,29 @@ package aci
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
|
"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/Azure/go-autorest/autorest/to"
|
||||||
"github.com/compose-spec/compose-go/types"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"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/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/api/containers"
|
"github.com/docker/compose-cli/api/containers"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"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/backend"
|
||||||
apicontext "github.com/docker/compose-cli/context"
|
apicontext "github.com/docker/compose-cli/context"
|
||||||
"github.com/docker/compose-cli/context/cloud"
|
"github.com/docker/compose-cli/context/cloud"
|
||||||
"github.com/docker/compose-cli/context/store"
|
"github.com/docker/compose-cli/context/store"
|
||||||
"github.com/docker/compose-cli/errdefs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
backendType = store.AciContextType
|
backendType = store.AciContextType
|
||||||
singleContainerTag = "docker-single-container"
|
singleContainerTag = "docker-single-container"
|
||||||
composeContainerTag = "docker-compose-application"
|
composeContainerTag = "docker-compose-application"
|
||||||
|
dockerVolumeTag = "docker-volume"
|
||||||
composeContainerSeparator = "_"
|
composeContainerSeparator = "_"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -109,12 +103,16 @@ func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
|
|||||||
aciComposeService: &aciComposeService{
|
aciComposeService: &aciComposeService{
|
||||||
ctx: aciCtx,
|
ctx: aciCtx,
|
||||||
},
|
},
|
||||||
|
aciVolumeService: &aciVolumeService{
|
||||||
|
aciContext: aciCtx,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type aciAPIService struct {
|
type aciAPIService struct {
|
||||||
*aciContainerService
|
*aciContainerService
|
||||||
*aciComposeService
|
*aciComposeService
|
||||||
|
*aciVolumeService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *aciAPIService) ContainerService() containers.Service {
|
func (a *aciAPIService) ContainerService() containers.Service {
|
||||||
@ -129,58 +127,8 @@ func (a *aciAPIService) SecretsService() secrets.Service {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type aciContainerService struct {
|
func (a *aciAPIService) VolumeService() volumes.Service {
|
||||||
ctx store.AciContext
|
return a.aciVolumeService
|
||||||
}
|
|
||||||
|
|
||||||
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 getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string {
|
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)
|
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) {
|
func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
|
||||||
if groupDefinition.Tags == nil {
|
if groupDefinition.Tags == nil {
|
||||||
groupDefinition.Tags = make(map[string]*string, 1)
|
groupDefinition.Tags = make(map[string]*string, 1)
|
||||||
@ -222,53 +150,6 @@ func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
|
|||||||
groupDefinition.Tags[tagName] = to.StringPtr(tagName)
|
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) {
|
func getGroupAndContainerName(containerID string) (string, string) {
|
||||||
tokens := strings.Split(containerID, composeContainerSeparator)
|
tokens := strings.Split(containerID, composeContainerSeparator)
|
||||||
groupName := tokens[0]
|
groupName := tokens[0]
|
||||||
@ -279,244 +160,3 @@ func getGroupAndContainerName(containerID string) (string, string) {
|
|||||||
}
|
}
|
||||||
return groupName, containerName
|
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
50
aci/cloud.go
Normal 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
126
aci/compose.go
Normal 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
253
aci/containers.go
Normal 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
|
||||||
|
}
|
@ -56,13 +56,8 @@ const (
|
|||||||
func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project) (containerinstance.ContainerGroup, error) {
|
func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project) (containerinstance.ContainerGroup, error) {
|
||||||
project := projectAciHelper(p)
|
project := projectAciHelper(p)
|
||||||
containerGroupName := strings.ToLower(project.Name)
|
containerGroupName := strings.ToLower(project.Name)
|
||||||
loginService, err := login.NewAzureLoginService()
|
storageHelper := login.StorageLogin{
|
||||||
if err != nil {
|
AciContext: aciContext,
|
||||||
return containerinstance.ContainerGroup{}, err
|
|
||||||
}
|
|
||||||
storageHelper := login.StorageAccountHelper{
|
|
||||||
LoginService: *loginService,
|
|
||||||
AciContext: aciContext,
|
|
||||||
}
|
}
|
||||||
volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
|
volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -205,7 +200,7 @@ func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, err
|
|||||||
return secretVolumes, nil
|
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))
|
azureFileVolumesMap := make(map[string]bool, len(p.Volumes))
|
||||||
var azureFileVolumesSlice []containerinstance.Volume
|
var azureFileVolumesSlice []containerinstance.Volume
|
||||||
for name, v := range p.Volumes {
|
for name, v := range p.Volumes {
|
||||||
|
@ -67,6 +67,19 @@ func NewStorageAccountsClient(subscriptionID string) (storage.AccountsClient, er
|
|||||||
return containerGroupsClient, nil
|
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
|
// NewSubscriptionsClient get subscription client
|
||||||
func NewSubscriptionsClient() (subscription.SubscriptionsClient, error) {
|
func NewSubscriptionsClient() (subscription.SubscriptionsClient, error) {
|
||||||
subc := subscription.NewSubscriptionsClient()
|
subc := subscription.NewSubscriptionsClient()
|
||||||
|
@ -25,14 +25,13 @@ import (
|
|||||||
"github.com/docker/compose-cli/context/store"
|
"github.com/docker/compose-cli/context/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageAccountHelper helper for Azure Storage Account
|
// StorageLogin helper for Azure Storage Login
|
||||||
type StorageAccountHelper struct {
|
type StorageLogin struct {
|
||||||
LoginService AzureLoginService
|
AciContext store.AciContext
|
||||||
AciContext store.AciContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAzureStorageAccountKey retrieves the storage account ket from the current azure login
|
// 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)
|
client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
234
aci/volumes.go
Normal file
234
aci/volumes.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/docker/compose-cli/api/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/api/containers"
|
"github.com/docker/compose-cli/api/containers"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"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/backend"
|
||||||
apicontext "github.com/docker/compose-cli/context"
|
apicontext "github.com/docker/compose-cli/context"
|
||||||
"github.com/docker/compose-cli/context/cloud"
|
"github.com/docker/compose-cli/context/cloud"
|
||||||
@ -86,3 +87,12 @@ func (c *Client) SecretsService() secrets.Service {
|
|||||||
|
|
||||||
return &secretsService{}
|
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
39
api/client/volume.go
Normal 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
37
api/volumes/api.go
Normal 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
|
||||||
|
}
|
@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/docker/compose-cli/api/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/api/containers"
|
"github.com/docker/compose-cli/api/containers"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"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/context/cloud"
|
||||||
"github.com/docker/compose-cli/errdefs"
|
"github.com/docker/compose-cli/errdefs"
|
||||||
)
|
)
|
||||||
@ -53,8 +54,9 @@ var backends = struct {
|
|||||||
// Service aggregates the service interfaces
|
// Service aggregates the service interfaces
|
||||||
type Service interface {
|
type Service interface {
|
||||||
ContainerService() containers.Service
|
ContainerService() containers.Service
|
||||||
SecretsService() secrets.Service
|
|
||||||
ComposeService() compose.Service
|
ComposeService() compose.Service
|
||||||
|
SecretsService() secrets.Service
|
||||||
|
VolumeService() volumes.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register adds a typed backend to the registry
|
// Register adds a typed backend to the registry
|
||||||
|
115
cli/cmd/volume/acivolume.go
Normal file
115
cli/cmd/volume/acivolume.go
Normal 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
66
cli/cmd/volume/list.go
Normal 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()
|
||||||
|
}
|
38
cli/cmd/volume/list_test.go
Normal file
38
cli/cmd/volume/list_test.go
Normal 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")
|
||||||
|
}
|
2
cli/cmd/volume/testdata/volumes-out.golden
vendored
Normal file
2
cli/cmd/volume/testdata/volumes-out.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ID DESCRIPTION
|
||||||
|
volume@123 volume 123
|
20
cli/main.go
20
cli/main.go
@ -28,23 +28,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/compose-cli/cli/cmd/compose"
|
"github.com/docker/compose-cli/cli/cmd/compose"
|
||||||
|
|
||||||
"github.com/docker/compose-cli/cli/cmd/logout"
|
"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/docker/compose-cli/errdefs"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
// Backend registrations
|
// Backend registrations
|
||||||
_ "github.com/docker/compose-cli/aci"
|
_ "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"
|
"github.com/docker/compose-cli/cli/cmd"
|
||||||
contextcmd "github.com/docker/compose-cli/cli/cmd/context"
|
contextcmd "github.com/docker/compose-cli/cli/cmd/context"
|
||||||
"github.com/docker/compose-cli/cli/cmd/login"
|
"github.com/docker/compose-cli/cli/cmd/login"
|
||||||
@ -54,6 +46,11 @@ import (
|
|||||||
"github.com/docker/compose-cli/config"
|
"github.com/docker/compose-cli/config"
|
||||||
apicontext "github.com/docker/compose-cli/context"
|
apicontext "github.com/docker/compose-cli/context"
|
||||||
"github.com/docker/compose-cli/context/store"
|
"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 (
|
var (
|
||||||
@ -182,6 +179,11 @@ func main() {
|
|||||||
ctype = cc.Type()
|
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())
|
metrics.Track(ctype, os.Args[1:], root.PersistentFlags())
|
||||||
|
|
||||||
ctx = apicontext.WithCurrentContext(ctx, currentContext)
|
ctx = apicontext.WithCurrentContext(ctx, currentContext)
|
||||||
|
@ -21,10 +21,10 @@ import (
|
|||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
|
||||||
"github.com/docker/compose-cli/api/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/api/containers"
|
"github.com/docker/compose-cli/api/containers"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"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/backend"
|
||||||
apicontext "github.com/docker/compose-cli/context"
|
apicontext "github.com/docker/compose-cli/context"
|
||||||
"github.com/docker/compose-cli/context/cloud"
|
"github.com/docker/compose-cli/context/cloud"
|
||||||
@ -97,6 +97,10 @@ func (a *ecsAPIService) SecretsService() secrets.Service {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ecsAPIService) VolumeService() volumes.Service {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getCloudService() (cloud.Service, error) {
|
func getCloudService() (cloud.Service, error) {
|
||||||
return ecsCloudService{}, nil
|
return ecsCloudService{}, nil
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,11 @@ import (
|
|||||||
"github.com/docker/compose-cli/api/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/api/containers"
|
"github.com/docker/compose-cli/api/containers"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"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/backend"
|
||||||
"github.com/docker/compose-cli/context/cloud"
|
"github.com/docker/compose-cli/context/cloud"
|
||||||
"github.com/docker/compose-cli/context/store"
|
"github.com/docker/compose-cli/context/store"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const backendType = store.EcsLocalSimulationContextType
|
const backendType = store.EcsLocalSimulationContextType
|
||||||
@ -58,6 +58,10 @@ func (e ecsLocalSimulation) ContainerService() containers.Service {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e ecsLocalSimulation) VolumeService() volumes.Service {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e ecsLocalSimulation) SecretsService() secrets.Service {
|
func (e ecsLocalSimulation) SecretsService() secrets.Service {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/docker/compose-cli/api/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/api/containers"
|
"github.com/docker/compose-cli/api/containers"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"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/backend"
|
||||||
"github.com/docker/compose-cli/context/cloud"
|
"github.com/docker/compose-cli/context/cloud"
|
||||||
"github.com/docker/compose-cli/errdefs"
|
"github.com/docker/compose-cli/errdefs"
|
||||||
@ -51,6 +52,10 @@ func (a *apiService) SecretsService() secrets.Service {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *apiService) VolumeService() volumes.Service {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
backend.Register("example", "example", service, cloud.NotImplementedCloudService)
|
backend.Register("example", "example", service, cloud.NotImplementedCloudService)
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ import (
|
|||||||
"github.com/docker/compose-cli/api/compose"
|
"github.com/docker/compose-cli/api/compose"
|
||||||
"github.com/docker/compose-cli/api/containers"
|
"github.com/docker/compose-cli/api/containers"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"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/backend"
|
||||||
"github.com/docker/compose-cli/context/cloud"
|
"github.com/docker/compose-cli/context/cloud"
|
||||||
"github.com/docker/compose-cli/errdefs"
|
"github.com/docker/compose-cli/errdefs"
|
||||||
@ -75,6 +76,10 @@ func (ms *local) SecretsService() secrets.Service {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ms *local) VolumeService() volumes.Service {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, error) {
|
func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, error) {
|
||||||
c, err := ms.apiClient.ContainerInspect(ctx, id)
|
c, err := ms.apiClient.ContainerInspect(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -39,7 +39,6 @@ import (
|
|||||||
"gotest.tools/v3/poll"
|
"gotest.tools/v3/poll"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
|
"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/azure-storage-file-go/azfile"
|
||||||
"github.com/Azure/go-autorest/autorest/to"
|
"github.com/Azure/go-autorest/autorest/to"
|
||||||
|
|
||||||
@ -48,7 +47,6 @@ import (
|
|||||||
"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"
|
||||||
"github.com/docker/compose-cli/tests/aci-e2e/storage"
|
|
||||||
. "github.com/docker/compose-cli/tests/framework"
|
. "github.com/docker/compose-cli/tests/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -131,7 +129,7 @@ func TestContainerRunVolume(t *testing.T) {
|
|||||||
sID, rg := setupTestResourceGroup(t, c)
|
sID, rg := setupTestResourceGroup(t, c)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testShareName = "dockertestshare"
|
fileshareName = "dockertestshare"
|
||||||
testFileContent = "Volume mounted successfully!"
|
testFileContent = "Volume mounted successfully!"
|
||||||
testFileName = "index.html"
|
testFileName = "index.html"
|
||||||
)
|
)
|
||||||
@ -142,31 +140,78 @@ func TestContainerRunVolume(t *testing.T) {
|
|||||||
Location: location,
|
Location: location,
|
||||||
ResourceGroup: rg,
|
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
|
// Used in subtests
|
||||||
var (
|
var (
|
||||||
container string
|
container string
|
||||||
hostIP string
|
hostIP string
|
||||||
endpoint 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) {
|
t.Run("run", func(t *testing.T) {
|
||||||
mountTarget := "/usr/share/nginx/html"
|
mountTarget := "/usr/share/nginx/html"
|
||||||
res := c.RunDockerCmd(
|
res := c.RunDockerCmd(
|
||||||
"run", "-d",
|
"run", "-d",
|
||||||
"-v", fmt.Sprintf("%s@%s:%s", saName, testShareName, mountTarget),
|
"-v", fmt.Sprintf("%s:%s", volumeID, mountTarget),
|
||||||
"-p", "80:80",
|
"-p", "80:80",
|
||||||
"nginx",
|
"nginx",
|
||||||
)
|
)
|
||||||
@ -189,7 +234,7 @@ func TestContainerRunVolume(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("ps", func(t *testing.T) {
|
t.Run("ps", func(t *testing.T) {
|
||||||
res := c.RunDockerCmd("ps")
|
res := c.RunDockerCmd("ps")
|
||||||
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
|
out := lines(res.Stdout())
|
||||||
l := out[len(out)-1]
|
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, container), "Looking for %q in line: %s", container, l)
|
||||||
assert.Assert(t, strings.Contains(l, "nginx"))
|
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) {
|
func TestContainerRunAttached(t *testing.T) {
|
||||||
c := NewParallelE2eCLI(t, binDir)
|
c := NewParallelE2eCLI(t, binDir)
|
||||||
_, _ = setupTestResourceGroup(t, c)
|
_, _ = setupTestResourceGroup(t, c)
|
||||||
@ -374,11 +423,11 @@ func TestContainerRunAttached(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("ps stopped container with --all", func(t *testing.T) {
|
t.Run("ps stopped container with --all", func(t *testing.T) {
|
||||||
res := c.RunDockerCmd("ps", container)
|
res := c.RunDockerCmd("ps", container)
|
||||||
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
|
out := lines(res.Stdout())
|
||||||
assert.Assert(t, is.Len(out, 1))
|
assert.Assert(t, is.Len(out, 1))
|
||||||
|
|
||||||
res = c.RunDockerCmd("ps", "--all", container)
|
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))
|
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"
|
// Name of Compose project is taken from current folder "acie2e"
|
||||||
c.RunDockerCmd("compose", "up", "-f", composeFile)
|
c.RunDockerCmd("compose", "up", "-f", composeFile)
|
||||||
res := c.RunDockerCmd("ps")
|
res := c.RunDockerCmd("ps")
|
||||||
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
|
out := lines(res.Stdout())
|
||||||
// Check three containers are running
|
// Check three containers are running
|
||||||
assert.Assert(t, is.Len(out, 4))
|
assert.Assert(t, is.Len(out, 4))
|
||||||
webRunning := false
|
webRunning := false
|
||||||
@ -444,7 +493,7 @@ func TestComposeUpUpdate(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("compose ps", func(t *testing.T) {
|
t.Run("compose ps", func(t *testing.T) {
|
||||||
res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName)
|
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))
|
assert.Assert(t, is.Len(lines, 4))
|
||||||
var wordsDisplayed, webDisplayed, dbDisplayed bool
|
var wordsDisplayed, webDisplayed, dbDisplayed bool
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
@ -468,7 +517,7 @@ func TestComposeUpUpdate(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("compose ls", func(t *testing.T) {
|
t.Run("compose ls", func(t *testing.T) {
|
||||||
res := c.RunDockerCmd("compose", "ls")
|
res := c.RunDockerCmd("compose", "ls")
|
||||||
lines := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
|
lines := lines(res.Stdout())
|
||||||
|
|
||||||
assert.Equal(t, 2, len(lines))
|
assert.Equal(t, 2, len(lines))
|
||||||
fields := strings.Fields(lines[1])
|
fields := strings.Fields(lines[1])
|
||||||
@ -485,7 +534,7 @@ func TestComposeUpUpdate(t *testing.T) {
|
|||||||
t.Run("update", func(t *testing.T) {
|
t.Run("update", func(t *testing.T) {
|
||||||
c.RunDockerCmd("compose", "up", "-f", composeFileMultiplePorts, "--project-name", composeProjectName)
|
c.RunDockerCmd("compose", "up", "-f", composeFileMultiplePorts, "--project-name", composeProjectName)
|
||||||
res := c.RunDockerCmd("ps")
|
res := c.RunDockerCmd("ps")
|
||||||
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
|
out := lines(res.Stdout())
|
||||||
// Check three containers are running
|
// Check three containers are running
|
||||||
assert.Assert(t, is.Len(out, 4))
|
assert.Assert(t, is.Len(out, 4))
|
||||||
|
|
||||||
@ -527,7 +576,7 @@ func TestComposeUpUpdate(t *testing.T) {
|
|||||||
t.Run("down", func(t *testing.T) {
|
t.Run("down", func(t *testing.T) {
|
||||||
c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
|
c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
|
||||||
res := c.RunDockerCmd("ps")
|
res := c.RunDockerCmd("ps")
|
||||||
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
|
out := lines(res.Stdout())
|
||||||
assert.Equal(t, len(out), 1)
|
assert.Equal(t, len(out), 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -548,7 +597,7 @@ func TestRunEnvVars(t *testing.T) {
|
|||||||
cmd.Env = append(cmd.Env, "MYSQL_USER=user1")
|
cmd.Env = append(cmd.Env, "MYSQL_USER=user1")
|
||||||
res := icmd.RunCmd(cmd)
|
res := icmd.RunCmd(cmd)
|
||||||
res.Assert(t, icmd.Success)
|
res.Assert(t, icmd.Success)
|
||||||
out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
|
out := lines(res.Stdout())
|
||||||
container := strings.TrimSpace(out[len(out)-1])
|
container := strings.TrimSpace(out[len(out)-1])
|
||||||
|
|
||||||
res = c.RunDockerCmd("inspect", container)
|
res = c.RunDockerCmd("inspect", container)
|
||||||
@ -583,7 +632,7 @@ func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string) {
|
|||||||
createAciContextAndUseIt(t, c, sID, rg)
|
createAciContextAndUseIt(t, c, sID, rg)
|
||||||
// Check nothing is running
|
// Check nothing is running
|
||||||
res := c.RunDockerCmd("ps")
|
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
|
return sID, rg
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -637,37 +686,6 @@ func createAciContextAndUseIt(t *testing.T, c *E2eCLI, sID, rgName string) {
|
|||||||
res.Assert(t, icmd.Expected{Out: contextName + " *"})
|
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) {
|
func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName, content string) {
|
||||||
fURL, err := url.Parse(baseURL + "/" + fileName)
|
fURL, err := url.Parse(baseURL + "/" + fileName)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
@ -677,7 +695,7 @@ func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getContainerName(stdout string) string {
|
func getContainerName(stdout string) string {
|
||||||
out := strings.Split(strings.TrimSpace(stdout), "\n")
|
out := lines(stdout)
|
||||||
return strings.TrimSpace(out[len(out)-1])
|
return strings.TrimSpace(out[len(out)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -21,8 +21,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/docker/compose-cli/tests/framework"
|
|
||||||
"gotest.tools/v3/icmd"
|
"gotest.tools/v3/icmd"
|
||||||
|
|
||||||
|
. "github.com/docker/compose-cli/tests/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user