From 03e418cbbb053c4efe309fc499c671607a5abd22 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Tue, 5 May 2020 10:58:24 +0200 Subject: [PATCH] Add compose up and down Signed-off-by: Ulysses Souza --- azure/aci.go | 8 ++++++ azure/backend.go | 45 +++++++++++++++++++++++------ cli/cmd/compose/compose.go | 58 ++++++++++++++++++++++++++++++++++++++ cli/cmd/exec.go | 2 +- cli/cmd/logs.go | 2 +- cli/cmd/ps.go | 2 +- cli/cmd/run/run.go | 2 +- cli/main.go | 2 ++ client/client.go | 12 ++++---- compose/api.go | 13 +++++++++ compose/project.go | 6 +++- server/proxy/proxy.go | 4 +-- 12 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 cli/cmd/compose/compose.go create mode 100644 compose/api.go diff --git a/azure/aci.go b/azure/aci.go index 4f1d53a59..39d2f1277 100644 --- a/azure/aci.go +++ b/azure/aci.go @@ -96,6 +96,14 @@ func createACIContainers(ctx context.Context, aciContext store.AciContext, group return containerGroup, err } +func deleteACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) (c containerinstance.ContainerGroup, err error) { + containerGroupsClient, err := getContainerGroupsClient(aciContext.SubscriptionID) + if err != nil { + return c, fmt.Errorf("cannot get container group client: %v", err) + } + return containerGroupsClient.Delete(ctx, aciContext.ResourceGroup, containerGroupName) +} + func execACIContainer(ctx context.Context, aciContext store.AciContext, command, containerGroup string, containerName string) (c containerinstance.ContainerExecResponse, err error) { containerClient, err := getContainerClient(aciContext.SubscriptionID) if err != nil { diff --git a/azure/backend.go b/azure/backend.go index 1e5d17c61..0e7bb6626 100644 --- a/azure/backend.go +++ b/azure/backend.go @@ -21,7 +21,7 @@ import ( "github.com/docker/api/context/store" ) -type containerService struct { +type aciApiService struct { containerGroupsClient containerinstance.ContainerGroupsClient ctx store.AciContext } @@ -36,8 +36,13 @@ func getter() interface{} { return &store.AciContext{} } +type AciService interface { + containers.ContainerService + compose.Service +} + // New creates a backend that can manage containers on ACI -func New(ctx context.Context) (containers.ContainerService, error) { +func New(ctx context.Context) (AciService, error) { currentContext := apicontext.CurrentContext(ctx) contextStore, err := store.New() if err != nil { @@ -53,13 +58,13 @@ func New(ctx context.Context) (containers.ContainerService, error) { containerGroupsClient := containerinstance.NewContainerGroupsClient(aciContext.SubscriptionID) containerGroupsClient.Authorizer = auth - return &containerService{ + return &aciApiService{ containerGroupsClient: containerGroupsClient, ctx: aciContext, }, nil } -func (cs *containerService) List(ctx context.Context) ([]containers.Container, error) { +func (cs *aciApiService) List(ctx context.Context) ([]containers.Container, error) { var containerGroups []containerinstance.ContainerGroup result, err := cs.containerGroupsClient.ListByResourceGroup(ctx, cs.ctx.ResourceGroup) if err != nil { @@ -73,7 +78,7 @@ func (cs *containerService) List(ctx context.Context) ([]containers.Container, e } } - res := []containers.Container{} + var res []containers.Container for _, containerGroup := range containerGroups { group, err := cs.containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, *containerGroup.Name) if err != nil { @@ -96,7 +101,7 @@ func (cs *containerService) List(ctx context.Context) ([]containers.Container, e return res, nil } -func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfig) error { +func (cs *aciApiService) Run(ctx context.Context, r containers.ContainerConfig) error { var ports []types.ServicePortConfig for _, p := range r.Ports { ports = append(ports, types.ServicePortConfig{ @@ -127,7 +132,7 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi return err } -func (cs *containerService) Exec(ctx context.Context, name string, command string, reader io.Reader, writer io.Writer) error { +func (cs *aciApiService) Exec(ctx context.Context, name string, command string, reader io.Reader, writer io.Writer) error { containerExecResponse, err := execACIContainer(ctx, cs.ctx, command, name, name) if err != nil { return err @@ -142,7 +147,7 @@ func (cs *containerService) Exec(ctx context.Context, name string, command strin ) } -func (cs *containerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error { +func (cs *aciApiService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error { logs, err := getACIContainerLogs(ctx, cs.ctx, containerName, containerName) if err != nil { return err @@ -163,3 +168,27 @@ func (cs *containerService) Logs(ctx context.Context, containerName string, req _, err = fmt.Fprint(req.Writer, logs) return err } + +func (cs *aciApiService) Up(ctx context.Context, opts compose.ProjectOptions) error { + project, err := compose.ProjectFromOptions(&opts) + if err != nil { + return err + } + logrus.Debugf("Up on project with name %q\n", project.Name) + groupDefinition, err := convert.ToContainerGroup(cs.ctx, *project) + if err != nil { + return err + } + _, err = createACIContainers(ctx, cs.ctx, groupDefinition) + return err +} + +func (cs *aciApiService) Down(ctx context.Context, opts compose.ProjectOptions) error { + project, err := compose.ProjectFromOptions(&opts) + if err != nil { + return err + } + logrus.Debugf("Down on project with name %q\n", project.Name) + _, err = deleteACIContainerGroup(ctx, cs.ctx, project.Name) + return err +} diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go new file mode 100644 index 000000000..885e4af58 --- /dev/null +++ b/cli/cmd/compose/compose.go @@ -0,0 +1,58 @@ +package compose + +import ( + "github.com/spf13/cobra" + + "github.com/docker/api/client" + "github.com/docker/api/compose" +) + +func Command() *cobra.Command { + command := &cobra.Command{ + Short: "Docker Compose", + Use: "compose", + } + command.AddCommand( + upCommand(), + downCommand(), + ) + return command +} + +func upCommand() *cobra.Command { + opts := &compose.ProjectOptions{} + upCmd := &cobra.Command{ + Use: "up", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := client.New(cmd.Context()) + if err != nil { + return err + } + return c.AciService().Up(cmd.Context(), *opts) + }, + } + upCmd.Flags().StringVar(&opts.Name, "name", "", "Project name") + upCmd.Flags().StringVar(&opts.WorkDir, "workdir", ".", "Work dir") + upCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files") + upCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables") + + return upCmd +} + +func downCommand() *cobra.Command { + opts := &compose.ProjectOptions{} + downCmd := &cobra.Command{ + Use: "down", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := client.New(cmd.Context()) + if err != nil { + return err + } + return c.AciService().Down(cmd.Context(), *opts) + }, + } + downCmd.Flags().StringVar(&opts.Name, "name", "", "Project name") + downCmd.Flags().StringVar(&opts.WorkDir, "workdir", ".", "Work dir") + + return downCmd +} \ No newline at end of file diff --git a/cli/cmd/exec.go b/cli/cmd/exec.go index a0199e209..ad024f4d2 100644 --- a/cli/cmd/exec.go +++ b/cli/cmd/exec.go @@ -62,5 +62,5 @@ func runExec(ctx context.Context, opts execOpts, name string, command string) er stdout = con } - return c.ContainerService().Exec(ctx, name, command, os.Stdin, stdout) + return c.AciService().Exec(ctx, name, command, os.Stdin, stdout) } diff --git a/cli/cmd/logs.go b/cli/cmd/logs.go index d9706d149..65c72bf6c 100644 --- a/cli/cmd/logs.go +++ b/cli/cmd/logs.go @@ -46,5 +46,5 @@ func runLogs(ctx context.Context, containerName string, opts logsOpts) error { Writer: os.Stdout, } - return c.ContainerService().Logs(ctx, containerName, req) + return c.AciService().Logs(ctx, containerName, req) } diff --git a/cli/cmd/ps.go b/cli/cmd/ps.go index 168dfa702..e54326cf8 100644 --- a/cli/cmd/ps.go +++ b/cli/cmd/ps.go @@ -23,7 +23,7 @@ var PsCommand = cobra.Command{ return errors.Wrap(err, "cannot connect to backend") } - containers, err := c.ContainerService().List(ctx) + containers, err := c.AciService().List(ctx) if err != nil { return errors.Wrap(err, "fetch containers") } diff --git a/cli/cmd/run/run.go b/cli/cmd/run/run.go index b2c25c1f7..b6fcb29d9 100644 --- a/cli/cmd/run/run.go +++ b/cli/cmd/run/run.go @@ -65,5 +65,5 @@ func runRun(ctx context.Context, image string, opts runOpts) error { return err } - return c.ContainerService().Run(ctx, project) + return c.AciService().Run(ctx, project) } diff --git a/cli/main.go b/cli/main.go index 098115c77..2b631f159 100644 --- a/cli/main.go +++ b/cli/main.go @@ -38,6 +38,7 @@ import ( // Backend registrations _ "github.com/docker/api/azure" + "github.com/docker/api/cli/cmd/compose" _ "github.com/docker/api/example" "github.com/sirupsen/logrus" @@ -102,6 +103,7 @@ func main() { run.Command(), cmd.ExecCommand(), cmd.LogsCommand(), + compose.Command(), ) helpFunc := root.HelpFunc() diff --git a/client/client.go b/client/client.go index 784259514..9771d06f6 100644 --- a/client/client.go +++ b/client/client.go @@ -31,11 +31,11 @@ import ( "context" "errors" + "github.com/docker/api/azure" "github.com/docker/api/backend" backendv1 "github.com/docker/api/backend/v1" cliv1 "github.com/docker/api/cli/v1" composev1 "github.com/docker/api/compose/v1" - "github.com/docker/api/containers" containersv1 "github.com/docker/api/containers/v1" apicontext "github.com/docker/api/context" "github.com/docker/api/context/store" @@ -57,13 +57,13 @@ func New(ctx context.Context) (*Client, error) { return nil, err } - ba, ok := b.(containers.ContainerService) + aciService, ok := b.(azure.AciService) if !ok { return nil, errors.New("backend not found") } return &Client{ backendType: contextType, - cc: ba, + cc: aciService, }, nil } @@ -76,10 +76,10 @@ type Client struct { composev1.ComposeClient backendType string - cc containers.ContainerService + cc azure.AciService } -// ContainerService returns the backend service for the current context -func (c *Client) ContainerService() containers.ContainerService { +// AciService returns the backend service for the current context +func (c *Client) AciService() azure.AciService { return c.cc } diff --git a/compose/api.go b/compose/api.go new file mode 100644 index 000000000..4bd19070d --- /dev/null +++ b/compose/api.go @@ -0,0 +1,13 @@ +package compose + +import ( + "context" +) + +// Service manages a compose project +type Service interface { + // Up executes the equivalent to a `compose up` + Up(ctx context.Context, opts ProjectOptions) error + // Down executes the equivalent to a `compose down` + Down(ctx context.Context, opts ProjectOptions) error +} diff --git a/compose/project.go b/compose/project.go index 94c669369..d3222a08a 100644 --- a/compose/project.go +++ b/compose/project.go @@ -51,7 +51,11 @@ func ProjectFromOptions(options *ProjectOptions) (*Project, error) { name := options.Name if name == "" { r := regexp.MustCompile(`[^a-z0-9\\-_]+`) - name = r.ReplaceAllString(strings.ToLower(filepath.Base(options.WorkDir)), "") + absPath, err := filepath.Abs(options.WorkDir) + if err != nil { + return nil, err + } + name = r.ReplaceAllString(strings.ToLower(filepath.Base(absPath)), "") } return newProject(types.ConfigDetails{ diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index a30c38801..43f48be89 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -31,7 +31,7 @@ type proxyContainerAPI struct{} func (p *proxyContainerAPI) List(ctx context.Context, _ *v1.ListRequest) (*v1.ListResponse, error) { client := Client(ctx) - c, err := client.ContainerService().List(ctx) + c, err := client.AciService().List(ctx) if err != nil { return &v1.ListResponse{}, nil } @@ -52,7 +52,7 @@ func (p *proxyContainerAPI) List(ctx context.Context, _ *v1.ListRequest) (*v1.Li func (p *proxyContainerAPI) Create(ctx context.Context, request *v1.CreateRequest) (*v1.CreateResponse, error) { client := Client(ctx) - err := client.ContainerService().Run(ctx, containers.ContainerConfig{ + err := client.AciService().Run(ctx, containers.ContainerConfig{ ID: request.Id, Image: request.Image, })