From 9c265f85e233a5609223f129de9a9bc1791dbb8e Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com> Date: Tue, 17 Sep 2024 22:13:13 +0330 Subject: [PATCH 1/4] feat: add export command Signed-off-by: MohammadHasan Akbari --- cmd/compose/compose.go | 1 + cmd/compose/export.go | 74 ++++++++++++++++++ docs/reference/compose.md | 1 + docs/reference/compose_export.md | 16 ++++ docs/reference/docker_compose.yaml | 2 + docs/reference/docker_compose_export.yaml | 45 +++++++++++ pkg/api/api.go | 9 +++ pkg/compose/export.go | 93 +++++++++++++++++++++++ pkg/e2e/export_test.go | 51 +++++++++++++ pkg/e2e/fixtures/export/compose.yaml | 9 +++ pkg/mocks/mock_docker_compose_api.go | 14 ++++ 11 files changed, 315 insertions(+) create mode 100644 cmd/compose/export.go create mode 100644 docs/reference/compose_export.md create mode 100644 docs/reference/docker_compose_export.yaml create mode 100644 pkg/compose/export.go create mode 100644 pkg/e2e/export_test.go create mode 100644 pkg/e2e/fixtures/export/compose.yaml diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 080d25f765d..78b7154cce2 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -594,6 +594,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli removeCommand(&opts, dockerCli, backend), execCommand(&opts, dockerCli, backend), attachCommand(&opts, dockerCli, backend), + exportCommand(&opts, dockerCli, backend), pauseCommand(&opts, dockerCli, backend), unpauseCommand(&opts, dockerCli, backend), topCommand(&opts, dockerCli, backend), diff --git a/cmd/compose/export.go b/cmd/compose/export.go new file mode 100644 index 00000000000..8ad08b7d2ae --- /dev/null +++ b/cmd/compose/export.go @@ -0,0 +1,74 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 compose + +import ( + "context" + + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" + + "github.com/docker/compose/v2/pkg/api" +) + +type exportOptions struct { + *ProjectOptions + + service string + output string + index int +} + +func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { + options := exportOptions{ + ProjectOptions: p, + } + cmd := &cobra.Command{ + Use: "export [OPTIONS] SERVICE", + Short: "Export a service container's filesystem as a tar archive", + Args: cobra.MinimumNArgs(1), + PreRunE: Adapt(func(ctx context.Context, args []string) error { + options.service = args[0] + return nil + }), + RunE: Adapt(func(ctx context.Context, args []string) error { + return runExport(ctx, dockerCli, backend, options) + }), + ValidArgsFunction: completeServiceNames(dockerCli, p), + } + + flags := cmd.Flags() + flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.") + flags.StringVarP(&options.output, "output", "o", "", "Write to a file, instead of STDOUT") + + return cmd +} + +func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error { + projectName, err := options.toProjectName(ctx, dockerCli) + if err != nil { + return err + } + + exportOptions := api.ExportOptions{ + Service: options.service, + Index: options.index, + Output: options.output, + } + + return backend.Export(ctx, projectName, exportOptions) +} diff --git a/docs/reference/compose.md b/docs/reference/compose.md index bb376edfcd3..5a69a01b508 100644 --- a/docs/reference/compose.md +++ b/docs/reference/compose.md @@ -19,6 +19,7 @@ Define and run multi-container applications with Docker | [`down`](compose_down.md) | Stop and remove containers, networks | | [`events`](compose_events.md) | Receive real time events from containers | | [`exec`](compose_exec.md) | Execute a command in a running container | +| [`export`](compose_export.md) | Export a service container's filesystem as a tar archive | | [`images`](compose_images.md) | List images used by the created containers | | [`kill`](compose_kill.md) | Force stop service containers | | [`logs`](compose_logs.md) | View output from containers | diff --git a/docs/reference/compose_export.md b/docs/reference/compose_export.md new file mode 100644 index 00000000000..942ea6a347f --- /dev/null +++ b/docs/reference/compose_export.md @@ -0,0 +1,16 @@ +# docker compose export + + +Export a service container's filesystem as a tar archive + +### Options + +| Name | Type | Default | Description | +|:-----------------|:---------|:--------|:---------------------------------------------------------| +| `--dry-run` | `bool` | | Execute command in dry run mode | +| `--index` | `int` | `0` | index of the container if service has multiple replicas. | +| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT | + + + + diff --git a/docs/reference/docker_compose.yaml b/docs/reference/docker_compose.yaml index 2c927b1d3c2..f59ec4a04b2 100644 --- a/docs/reference/docker_compose.yaml +++ b/docs/reference/docker_compose.yaml @@ -13,6 +13,7 @@ cname: - docker compose down - docker compose events - docker compose exec + - docker compose export - docker compose images - docker compose kill - docker compose logs @@ -44,6 +45,7 @@ clink: - docker_compose_down.yaml - docker_compose_events.yaml - docker_compose_exec.yaml + - docker_compose_export.yaml - docker_compose_images.yaml - docker_compose_kill.yaml - docker_compose_logs.yaml diff --git a/docs/reference/docker_compose_export.yaml b/docs/reference/docker_compose_export.yaml new file mode 100644 index 00000000000..5dfb3be0a47 --- /dev/null +++ b/docs/reference/docker_compose_export.yaml @@ -0,0 +1,45 @@ +command: docker compose export +short: Export a service container's filesystem as a tar archive +long: Export a service container's filesystem as a tar archive +usage: docker compose export [OPTIONS] SERVICE +pname: docker compose +plink: docker_compose.yaml +options: + - option: index + value_type: int + default_value: "0" + description: index of the container if service has multiple replicas. + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false + - option: output + shorthand: o + value_type: string + description: Write to a file, instead of STDOUT + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false +inherited_options: + - option: dry-run + value_type: bool + default_value: "false" + description: Execute command in dry run mode + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false +deprecated: false +hidden: false +experimental: false +experimentalcli: false +kubernetes: false +swarm: false + diff --git a/pkg/api/api.go b/pkg/api/api.go index 4ae36ed3bed..3fc1e572176 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -90,6 +90,8 @@ type Service interface { Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error) // Scale manages numbers of container instances running per service Scale(ctx context.Context, project *types.Project, options ScaleOptions) error + // Export a service container's filesystem as a tar archive + Export(ctx context.Context, projectName string, options ExportOptions) error } type ScaleOptions struct { @@ -553,6 +555,13 @@ type PauseOptions struct { Project *types.Project } +// ExportOptions group options of the Export API +type ExportOptions struct { + Service string + Index int + Output string +} + const ( // STARTING indicates that stack is being deployed STARTING string = "Starting" diff --git a/pkg/compose/export.go b/pkg/compose/export.go new file mode 100644 index 00000000000..8a7248959f3 --- /dev/null +++ b/pkg/compose/export.go @@ -0,0 +1,93 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 compose + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/docker/cli/cli/command" + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/progress" + "github.com/pkg/errors" +) + +func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error { + return progress.RunWithTitle(ctx, func(ctx context.Context) error { + return s.export(ctx, projectName, options) + }, s.stdinfo(), "Exporting") +} + +func (s *composeService) export(ctx context.Context, projectName string, options api.ExportOptions) error { + projectName = strings.ToLower(projectName) + + container, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, options.Service, options.Index) + if err != nil { + return err + } + + if options.Output == "" && s.dockerCli.Out().IsTerminal() { + return errors.New("output option is required when exporting to terminal") + } + + if err := command.ValidateOutputPath(options.Output); err != nil { + return errors.Wrap(err, "failed to export container") + } + + clnt := s.dockerCli.Client() + + w := progress.ContextWriter(ctx) + + name := getCanonicalContainerName(container) + msg := fmt.Sprintf("export %s to %s", name, options.Output) + + w.Event(progress.Event{ + ID: name, + Text: msg, + Status: progress.Working, + StatusText: "Exporting", + }) + + responseBody, err := clnt.ContainerExport(ctx, container.ID) + if err != nil { + return err + } + + defer responseBody.Close() + + if !s.dryRun { + if options.Output == "" { + _, err := io.Copy(s.dockerCli.Out(), responseBody) + return err + } + + if err = command.CopyToFile(options.Output, responseBody); err != nil { + return err + } + } + + w.Event(progress.Event{ + ID: name, + Text: msg, + Status: progress.Done, + StatusText: "Exported", + }) + + return nil +} diff --git a/pkg/e2e/export_test.go b/pkg/e2e/export_test.go new file mode 100644 index 00000000000..58e57b576c3 --- /dev/null +++ b/pkg/e2e/export_test.go @@ -0,0 +1,51 @@ +/* + Copyright 2023 Docker Compose CLI authors + + 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 e2e + +import ( + "testing" +) + +func TestExport(t *testing.T) { + const projectName = "e2e-export-service" + c := NewParallelCLI(t) + + cleanup := func() { + c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") + } + t.Cleanup(cleanup) + cleanup() + + c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service") + c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "service.tar", "service") +} + +func TestExportWithReplicas(t *testing.T) { + const projectName = "e2e-export-service-with-replicas" + c := NewParallelCLI(t) + + cleanup := func() { + c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") + } + t.Cleanup(cleanup) + cleanup() + + c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") + c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r1.tar", "--index=1", "service-with-replicas") + c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r2.tar", "--index=2", "service-with-replicas") +} + diff --git a/pkg/e2e/fixtures/export/compose.yaml b/pkg/e2e/fixtures/export/compose.yaml new file mode 100644 index 00000000000..28e4b15bd68 --- /dev/null +++ b/pkg/e2e/fixtures/export/compose.yaml @@ -0,0 +1,9 @@ +services: + service: + image: alpine + command: sleep infinity + service-with-replicas: + image: alpine + command: sleep infinity + deploy: + replicas: 3 \ No newline at end of file diff --git a/pkg/mocks/mock_docker_compose_api.go b/pkg/mocks/mock_docker_compose_api.go index 1390a85fb7e..858c6e7b13a 100644 --- a/pkg/mocks/mock_docker_compose_api.go +++ b/pkg/mocks/mock_docker_compose_api.go @@ -155,6 +155,20 @@ func (mr *MockServiceMockRecorder) Exec(ctx, projectName, options any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockService)(nil).Exec), ctx, projectName, options) } +// Export mocks base method. +func (m *MockService) Export(ctx context.Context, projectName string, options api.ExportOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Export", ctx, projectName, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// Export indicates an expected call of Export. +func (mr *MockServiceMockRecorder) Export(ctx, projectName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Export", reflect.TypeOf((*MockService)(nil).Export), ctx, projectName, options) +} + // Images mocks base method. func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) { m.ctrl.T.Helper() From b82add89584356fcb927ae99d60c89b29526b68a Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:36:17 +0000 Subject: [PATCH 2/4] fix: lint Signed-off-by: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com> Signed-off-by: MohammadHasan Akbari --- pkg/compose/export.go | 15 ++++++++++++--- pkg/e2e/export_test.go | 43 +++++++++++++++++++++--------------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/pkg/compose/export.go b/pkg/compose/export.go index 8a7248959f3..18ce4237e68 100644 --- a/pkg/compose/export.go +++ b/pkg/compose/export.go @@ -69,15 +69,24 @@ func (s *composeService) export(ctx context.Context, projectName string, options return err } - defer responseBody.Close() + defer func() { + if err := responseBody.Close(); err != nil { + w.Event(progress.Event{ + ID: name, + Text: msg, + Status: progress.Error, + StatusText: fmt.Sprintf("Failed to close response body: %v", err), + }) + } + }() if !s.dryRun { if options.Output == "" { _, err := io.Copy(s.dockerCli.Out(), responseBody) return err } - - if err = command.CopyToFile(options.Output, responseBody); err != nil { + + if err := command.CopyToFile(options.Output, responseBody); err != nil { return err } } diff --git a/pkg/e2e/export_test.go b/pkg/e2e/export_test.go index 58e57b576c3..baa0dc5b94c 100644 --- a/pkg/e2e/export_test.go +++ b/pkg/e2e/export_test.go @@ -21,31 +21,30 @@ import ( ) func TestExport(t *testing.T) { - const projectName = "e2e-export-service" - c := NewParallelCLI(t) + const projectName = "e2e-export-service" + c := NewParallelCLI(t) - cleanup := func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") - } - t.Cleanup(cleanup) - cleanup() + cleanup := func() { + c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") + } + t.Cleanup(cleanup) + cleanup() - c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "service.tar", "service") + c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service") + c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "service.tar", "service") } func TestExportWithReplicas(t *testing.T) { - const projectName = "e2e-export-service-with-replicas" - c := NewParallelCLI(t) - - cleanup := func() { - c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") - } - t.Cleanup(cleanup) - cleanup() - - c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r1.tar", "--index=1", "service-with-replicas") - c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r2.tar", "--index=2", "service-with-replicas") + const projectName = "e2e-export-service-with-replicas" + c := NewParallelCLI(t) + + cleanup := func() { + c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans") + } + t.Cleanup(cleanup) + cleanup() + + c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") + c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r1.tar", "--index=1", "service-with-replicas") + c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r2.tar", "--index=2", "service-with-replicas") } - From 3189f4d9354550b74859a28225a63a15e2cd82d9 Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:38:12 +0000 Subject: [PATCH 3/4] fix: validate-go-mod Signed-off-by: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com> Signed-off-by: MohammadHasan Akbari --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index da8a67a3292..f4c23c23aa4 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 github.com/otiai10/copy v1.14.0 + github.com/pkg/errors v0.9.1 github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc github.com/sirupsen/logrus v1.9.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 @@ -143,7 +144,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect From 29daf847fa667133a2f1268c3926aef4645e5616 Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:09:45 +0000 Subject: [PATCH 4/4] chore: remove errors depricated pkg Signed-off-by: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com> Signed-off-by: MohammadHasan Akbari --- go.mod | 2 +- pkg/compose/export.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index f4c23c23aa4..da8a67a3292 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 github.com/otiai10/copy v1.14.0 - github.com/pkg/errors v0.9.1 github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc github.com/sirupsen/logrus v1.9.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 @@ -144,6 +143,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect diff --git a/pkg/compose/export.go b/pkg/compose/export.go index 18ce4237e68..795208ae191 100644 --- a/pkg/compose/export.go +++ b/pkg/compose/export.go @@ -25,7 +25,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" - "github.com/pkg/errors" ) func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error { @@ -43,11 +42,11 @@ func (s *composeService) export(ctx context.Context, projectName string, options } if options.Output == "" && s.dockerCli.Out().IsTerminal() { - return errors.New("output option is required when exporting to terminal") + return fmt.Errorf("output option is required when exporting to terminal") } if err := command.ValidateOutputPath(options.Output); err != nil { - return errors.Wrap(err, "failed to export container") + return fmt.Errorf("failed to export container: %w", err) } clnt := s.dockerCli.Client()