From 15addf5c22899e6767a9b957063f7cb57d5d650d Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Tue, 8 Sep 2020 10:11:02 +0200 Subject: [PATCH] Break out aci backend.go into several files for each service Signed-off-by: Guillaume Tardif --- aci/aci.go | 28 ++ aci/backend.go | 394 +------------------- aci/cloud.go | 48 +++ aci/compose.go | 128 +++++++ aci/containers.go | 254 +++++++++++++ aci/convert/convert.go | 4 +- aci/login/storagelogin.go | 49 +++ aci/{login/storage_helper.go => volumes.go} | 80 ++-- 8 files changed, 544 insertions(+), 441 deletions(-) create mode 100644 aci/cloud.go create mode 100644 aci/compose.go create mode 100644 aci/containers.go create mode 100644 aci/login/storagelogin.go rename aci/{login/storage_helper.go => volumes.go} (53%) diff --git a/aci/aci.go b/aci/aci.go index 4a251d3f..795a8f49 100644 --- a/aci/aci.go +++ b/aci/aci.go @@ -129,6 +129,34 @@ func getACIContainerGroup(ctx context.Context, aciContext store.AciContext, cont return containerGroupsClient.Get(ctx, aciContext.ResourceGroup, containerGroupName) } +func getACIContainerGroups(ctx context.Context, subscriptionID string, resourceGroup string) ([]containerinstance.ContainerGroup, error) { + groupsClient, err := login.NewContainerGroupsClient(subscriptionID) + if err != nil { + return nil, err + } + var containerGroups []containerinstance.ContainerGroup + result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup) + if err != nil { + return nil, err + } + + for result.NotDone() { + containerGroups = append(containerGroups, result.Values()...) + if err := result.NextWithContext(ctx); err != nil { + return nil, err + } + } + var groups []containerinstance.ContainerGroup + for _, group := range containerGroups { + group, err := groupsClient.Get(ctx, resourceGroup, *group.Name) + if err != nil { + return nil, err + } + groups = append(groups, group) + } + return groups, nil +} + func deleteACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) (containerinstance.ContainerGroup, error) { containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID) if err != nil { diff --git a/aci/backend.go b/aci/backend.go index 19e38664..955c487a 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -18,18 +18,11 @@ package aci import ( "context" - "fmt" - "io" - "net/http" - "strconv" "strings" "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance" - "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" - "github.com/compose-spec/compose-go/types" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/docker/compose-cli/aci/convert" "github.com/docker/compose-cli/aci/login" @@ -41,7 +34,6 @@ import ( apicontext "github.com/docker/compose-cli/context" "github.com/docker/compose-cli/context/cloud" "github.com/docker/compose-cli/context/store" - "github.com/docker/compose-cli/errdefs" ) const ( @@ -111,7 +103,7 @@ func getAciAPIService(aciCtx store.AciContext) *aciAPIService { ctx: aciCtx, }, aciVolumeService: &aciVolumeService{ - ctx: aciCtx, + aciContext: aciCtx, }, } } @@ -138,60 +130,6 @@ func (a *aciAPIService) VolumeService() volumes.Service { return a.aciVolumeService } -type aciContainerService struct { - ctx store.AciContext -} - -func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) { - containerGroups, err := getContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup) - if err != nil { - return []containers.Container{}, err - } - var res []containers.Container - for _, group := range containerGroups { - if group.Containers == nil || len(*group.Containers) < 1 { - return []containers.Container{}, fmt.Errorf("no containers found in ACI container group %s", *group.Name) - } - - for _, container := range *group.Containers { - if isContainerVisible(container, group, all) { - continue - } - c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container) - res = append(res, c) - } - } - return res, nil -} - -func getContainerGroups(ctx context.Context, subscriptionID string, resourceGroup string) ([]containerinstance.ContainerGroup, error) { - groupsClient, err := login.NewContainerGroupsClient(subscriptionID) - if err != nil { - return nil, err - } - var containerGroups []containerinstance.ContainerGroup - result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup) - if err != nil { - return 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 getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string { containerID := *group.Name + composeContainerSeparator + *container.Name if _, ok := group.Tags[singleContainerTag]; ok { @@ -204,26 +142,6 @@ func isContainerVisible(container containerinstance.Container, group containerin return *container.Name == convert.ComposeDNSSidecarName || (!showAll && convert.GetStatus(container, group) != convert.StatusRunning) } -func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error { - if strings.Contains(r.ID, composeContainerSeparator) { - return errors.New(fmt.Sprintf("invalid container name. ACI container name cannot include %q", composeContainerSeparator)) - } - - project, err := convert.ContainerToComposeProject(r) - if err != nil { - return err - } - - logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID) - groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project) - if err != nil { - return err - } - addTag(&groupDefinition, singleContainerTag) - - return createACIContainers(ctx, cs.ctx, groupDefinition) -} - func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) { if groupDefinition.Tags == nil { groupDefinition.Tags = make(map[string]*string, 1) @@ -231,52 +149,6 @@ func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) { groupDefinition.Tags[tagName] = to.StringPtr(tagName) } -func (cs *aciContainerService) Start(ctx context.Context, containerID string) error { - groupName, containerName := getGroupAndContainerName(containerID) - if groupName != containerID { - msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s" - return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName)) - } - - containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID) - if err != nil { - return err - } - - future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName) - if err != nil { - var aerr autorest.DetailedError - if ok := errors.As(err, &aerr); ok { - if aerr.StatusCode == http.StatusNotFound { - return errdefs.ErrNotFound - } - } - return err - } - - return future.WaitForCompletionRef(ctx, containerGroupsClient.Client) -} - -func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error { - if timeout != nil && *timeout != uint32(0) { - return errors.Errorf("ACI integration does not support setting a timeout to stop a container before killing it.") - } - groupName, containerName := getGroupAndContainerName(containerID) - if groupName != containerID { - msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s" - return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName)) - } - return stopACIContainerGroup(ctx, cs.ctx, groupName) -} - -func (cs *aciContainerService) Kill(ctx context.Context, containerID string, _ string) error { - groupName, containerName := getGroupAndContainerName(containerID) - if groupName != containerID { - msg := "cannot kill service %q from compose application %q, you can kill the entire compose app with docker kill %s" - return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName)) - } - return stopACIContainerGroup(ctx, cs.ctx, groupName) // As ACI doesn't have a kill command, we are using the stop implementation instead -} func getGroupAndContainerName(containerID string) (string, string) { tokens := strings.Split(containerID, composeContainerSeparator) @@ -289,267 +161,3 @@ func getGroupAndContainerName(containerID string) (string, string) { return groupName, containerName } -func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error { - err := verifyExecCommand(request.Command) - if err != nil { - return err - } - groupName, containerAciName := getGroupAndContainerName(name) - containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName) - if err != nil { - return err - } - - return exec( - context.Background(), - *containerExecResponse.WebSocketURI, - *containerExecResponse.Password, - request, - ) -} - -func verifyExecCommand(command string) error { - tokens := strings.Split(command, " ") - if len(tokens) > 1 { - return errors.New("ACI exec command does not accept arguments to the command. " + - "Only the binary should be specified") - } - return nil -} - -func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error { - groupName, containerAciName := getGroupAndContainerName(containerName) - var tail *int32 - - if req.Follow { - return streamLogs(ctx, cs.ctx, groupName, containerAciName, req) - } - - if req.Tail != "all" { - reqTail, err := strconv.Atoi(req.Tail) - if err != nil { - return err - } - i32 := int32(reqTail) - tail = &i32 - } - - logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail) - if err != nil { - return err - } - - _, err = fmt.Fprint(req.Writer, logs) - return err -} - -func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error { - groupName, containerName := getGroupAndContainerName(containerID) - if groupName != containerID { - msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s" - return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName)) - } - - if !request.Force { - containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID) - if err != nil { - return err - } - - cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName) - if err != nil { - if cg.StatusCode == http.StatusNotFound { - return errdefs.ErrNotFound - } - return err - } - - for _, container := range *cg.Containers { - status := convert.GetStatus(container, cg) - - if status == convert.StatusRunning { - return errdefs.ErrForbidden - } - } - } - - cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName) - // Delete returns `StatusNoContent` if the group is not found - if cg.StatusCode == http.StatusNoContent { - return errdefs.ErrNotFound - } - if err != nil { - return err - } - - return err -} - -func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) { - groupName, containerName := getGroupAndContainerName(containerID) - - cg, err := getACIContainerGroup(ctx, cs.ctx, groupName) - if err != nil { - return containers.Container{}, err - } - if cg.StatusCode == http.StatusNoContent { - return containers.Container{}, errdefs.ErrNotFound - } - - var cc containerinstance.Container - var found = false - for _, c := range *cg.Containers { - if to.String(c.Name) == containerName { - cc = c - found = true - break - } - } - if !found { - return containers.Container{}, errdefs.ErrNotFound - } - - return convert.ContainerGroupToContainer(containerID, cg, cc), nil -} - -type aciComposeService struct { - ctx store.AciContext -} - -func (cs *aciComposeService) Up(ctx context.Context, project *types.Project) error { - logrus.Debugf("Up on project with name %q\n", project.Name) - groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project) - addTag(&groupDefinition, composeContainerTag) - - if err != nil { - return err - } - return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition) -} - -func (cs *aciComposeService) Down(ctx context.Context, project string) error { - logrus.Debugf("Down on project with name %q\n", project) - - cg, err := deleteACIContainerGroup(ctx, cs.ctx, project) - if err != nil { - return err - } - if cg.StatusCode == http.StatusNoContent { - return errdefs.ErrNotFound - } - - return err -} - -func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) { - groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID) - if err != nil { - return nil, err - } - - group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, project) - if err != nil { - return []compose.ServiceStatus{}, err - } - - if group.Containers == nil || len(*group.Containers) < 1 { - return []compose.ServiceStatus{}, fmt.Errorf("no containers found in ACI container group %s", project) - } - - res := []compose.ServiceStatus{} - for _, container := range *group.Containers { - if isContainerVisible(container, group, false) { - continue - } - res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container)) - } - return res, nil -} - -func (cs *aciComposeService) List(ctx context.Context, project string) ([]compose.Stack, error) { - containerGroups, err := getContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup) - if err != nil { - return []compose.Stack{}, err - } - - stacks := []compose.Stack{} - for _, group := range containerGroups { - if _, found := group.Tags[composeContainerTag]; !found { - continue - } - if project != "" && *group.Name != project { - continue - } - state := compose.RUNNING - for _, container := range *group.ContainerGroupProperties.Containers { - containerState := convert.GetStatus(container, group) - if containerState != compose.RUNNING { - state = containerState - break - } - } - stacks = append(stacks, compose.Stack{ - ID: *group.ID, - Name: *group.Name, - Status: state, - }) - } - return stacks, nil -} - -func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writer) error { - return errdefs.ErrNotImplemented -} - -func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project) ([]byte, error) { - return nil, errdefs.ErrNotImplemented -} - -type aciVolumeService struct { - ctx store.AciContext -} - -func (cs *aciVolumeService) List(ctx context.Context) ([]volumes.Volume, error) { - storageHelper := login.StorageAccountHelper{AciContext: cs.ctx} - return storageHelper.ListFileShare(ctx) -} - -//VolumeCreateOptions options to create a new ACI volume -type VolumeCreateOptions struct { - Account string - Fileshare string -} - -func (cs *aciVolumeService) Create(ctx context.Context, options interface{}) (volumes.Volume, error) { - opts, ok := options.(VolumeCreateOptions) - if !ok { - return volumes.Volume{}, errors.New("Could not read azure LoginParams struct from generic parameter") - } - storageHelper := login.StorageAccountHelper{AciContext: cs.ctx} - return storageHelper.CreateFileShare(ctx, opts.Account, opts.Fileshare) -} - -type aciCloudService struct { - 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) -} diff --git a/aci/cloud.go b/aci/cloud.go new file mode 100644 index 00000000..2d2e9757 --- /dev/null +++ b/aci/cloud.go @@ -0,0 +1,48 @@ +/* + 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) +} diff --git a/aci/compose.go b/aci/compose.go new file mode 100644 index 00000000..07759a73 --- /dev/null +++ b/aci/compose.go @@ -0,0 +1,128 @@ +/* + 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\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 := getACIContainerGroups(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 +} + + diff --git a/aci/containers.go b/aci/containers.go new file mode 100644 index 00000000..08cacbc3 --- /dev/null +++ b/aci/containers.go @@ -0,0 +1,254 @@ +/* + 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 []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 (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 (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 (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 +} diff --git a/aci/convert/convert.go b/aci/convert/convert.go index 65ce5683..d35b410b 100644 --- a/aci/convert/convert.go +++ b/aci/convert/convert.go @@ -56,7 +56,7 @@ const ( func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project) (containerinstance.ContainerGroup, error) { project := projectAciHelper(p) containerGroupName := strings.ToLower(project.Name) - storageHelper := login.StorageAccountHelper{ + storageHelper := login.StorageLogin{ AciContext: aciContext, } volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper) @@ -200,7 +200,7 @@ func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, err return secretVolumes, nil } -func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageAccountHelper) (map[string]bool, []containerinstance.Volume, error) { +func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageLogin) (map[string]bool, []containerinstance.Volume, error) { azureFileVolumesMap := make(map[string]bool, len(p.Volumes)) var azureFileVolumesSlice []containerinstance.Volume for name, v := range p.Volumes { diff --git a/aci/login/storagelogin.go b/aci/login/storagelogin.go new file mode 100644 index 00000000..446eb56d --- /dev/null +++ b/aci/login/storagelogin.go @@ -0,0 +1,49 @@ +/* + 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 login + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + + "github.com/docker/compose-cli/context/store" +) + +// StorageLogin helper for Azure Storage Login +type StorageLogin struct { + AciContext store.AciContext +} + +// GetAzureStorageAccountKey retrieves the storage account ket from the current azure login +func (helper StorageLogin) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) { + client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID) + if err != nil { + return "", err + } + result, err := client.ListKeys(ctx, helper.AciContext.ResourceGroup, accountName, "") + if err != nil { + return "", errors.Wrap(err, fmt.Sprintf("could not access storage account acountKeys for %s, using the azure login", accountName)) + } + if result.Keys != nil && len((*result.Keys)) < 1 { + return "", fmt.Errorf("no key could be obtained for storage account %s from your azure login", accountName) + } + + key := (*result.Keys)[0] + return *key.Value, nil +} \ No newline at end of file diff --git a/aci/login/storage_helper.go b/aci/volumes.go similarity index 53% rename from aci/login/storage_helper.go rename to aci/volumes.go index de81c5b3..3ce505bc 100644 --- a/aci/login/storage_helper.go +++ b/aci/volumes.go @@ -14,11 +14,12 @@ limitations under the License. */ -package login +package aci import ( "context" "fmt" + "github.com/docker/compose-cli/aci/login" "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" @@ -30,48 +31,27 @@ import ( "github.com/docker/compose-cli/context/store" ) -// StorageAccountHelper helper for Azure Storage Account -type StorageAccountHelper struct { - AciContext store.AciContext +type aciVolumeService struct { + aciContext store.AciContext } -// GetAzureStorageAccountKey retrieves the storage account ket from the current azure login -func (helper StorageAccountHelper) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) { - client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID) - if err != nil { - return "", err - } - result, err := client.ListKeys(ctx, helper.AciContext.ResourceGroup, accountName, "") - if err != nil { - return "", errors.Wrap(err, fmt.Sprintf("could not access storage account acountKeys for %s, using the azure login", accountName)) - } - if result.Keys != nil && len((*result.Keys)) < 1 { - return "", fmt.Errorf("no key could be obtained for storage account %s from your azure login", accountName) - } - - key := (*result.Keys)[0] - return *key.Value, nil -} - -// ListFileShare list file shares in all visible storage accounts -func (helper StorageAccountHelper) ListFileShare(ctx context.Context) ([]volumes.Volume, error) { - aciContext := helper.AciContext - accountClient, err := NewStorageAccountsClient(aciContext.SubscriptionID) +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, aciContext.ResourceGroup) + result, err := accountClient.ListByResourceGroup(ctx, cs.aciContext.ResourceGroup) if err != nil { return nil, err } accounts := result.Value - fileShareClient, err := NewFileShareClient(aciContext.SubscriptionID) + 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, aciContext.ResourceGroup, *account.Name, "", "", "") + fileSharePage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, *account.Name, "", "", "") if err != nil { return nil, err } @@ -89,30 +69,30 @@ func (helper StorageAccountHelper) ListFileShare(ctx context.Context) ([]volumes return fileShares, nil } -func toVolume(account storage.Account, fileShareName string) volumes.Volume { - return volumes.Volume{ - ID: fmt.Sprintf("%s@%s", *account.Name, fileShareName), - Name: fileShareName, - Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name), - } +//VolumeCreateOptions options to create a new ACI volume +type VolumeCreateOptions struct { + Account string + Fileshare string } -// CreateFileShare create a new fileshare -func (helper StorageAccountHelper) CreateFileShare(ctx context.Context, accountName string, fileShareName string) (volumes.Volume, error) { - aciContext := helper.AciContext - accountClient, err := NewStorageAccountsClient(aciContext.SubscriptionID) +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 LoginParams struct from generic parameter") + } + accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID) if err != nil { return volumes.Volume{}, err } - account, err := accountClient.GetProperties(ctx, aciContext.ResourceGroup, accountName, "") + account, err := accountClient.GetProperties(ctx, cs.aciContext.ResourceGroup, opts.Account, "") if err != nil { if account.StatusCode != 404 { return volumes.Volume{}, err } //TODO confirm storage account creation - parameters := defaultStorageAccountParams(aciContext) + parameters := defaultStorageAccountParams(cs.aciContext) // TODO progress account creation - future, err := accountClient.Create(ctx, aciContext.ResourceGroup, accountName, parameters) + future, err := accountClient.Create(ctx, cs.aciContext.ResourceGroup, opts.Account, parameters) if err != nil { return volumes.Volume{}, err } @@ -121,25 +101,33 @@ func (helper StorageAccountHelper) CreateFileShare(ctx context.Context, accountN return volumes.Volume{}, err } } - fileShareClient, err := NewFileShareClient(aciContext.SubscriptionID) + fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID) if err != nil { return volumes.Volume{}, err } - fileShare, err := fileShareClient.Get(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, "") + fileShare, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, "") if err == nil { - return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", fileShareName) + return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", opts.Fileshare) } if fileShare.StatusCode != 404 { return volumes.Volume{}, err } - fileShare, err = fileShareClient.Create(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, storage.FileShare{}) + fileShare, err = fileShareClient.Create(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, storage.FileShare{}) if err != nil { return volumes.Volume{}, err } return toVolume(account, *fileShare.Name), nil } +func toVolume(account storage.Account, fileShareName string) volumes.Volume { + return volumes.Volume{ + ID: fmt.Sprintf("%s@%s", *account.Name, fileShareName), + Name: fileShareName, + Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name), + } +} + func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters { return storage.AccountCreateParameters{ Location: &aciContext.Location,