From 03615abda1406d8c842d6ef7aae5fd11e83c8441 Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari Date: Tue, 5 Nov 2024 22:04:30 +0330 Subject: [PATCH 1/2] feat: add commit command Signed-off-by: MohammadHasan Akbari --- cmd/compose/commit.go | 93 +++++++++++++++++++++++ cmd/compose/compose.go | 1 + docs/reference/compose.md | 1 + docs/reference/compose_commit.md | 19 +++++ docs/reference/docker_compose.yaml | 2 + docs/reference/docker_compose_commit.yaml | 76 ++++++++++++++++++ pkg/api/api.go | 16 ++++ pkg/compose/commit.go | 87 +++++++++++++++++++++ pkg/e2e/commit_test.go | 93 +++++++++++++++++++++++ pkg/e2e/fixtures/commit/compose.yaml | 9 +++ pkg/mocks/mock_docker_compose_api.go | 14 ++++ 11 files changed, 411 insertions(+) create mode 100644 cmd/compose/commit.go create mode 100644 docs/reference/compose_commit.md create mode 100644 docs/reference/docker_compose_commit.yaml create mode 100644 pkg/compose/commit.go create mode 100644 pkg/e2e/commit_test.go create mode 100644 pkg/e2e/fixtures/commit/compose.yaml diff --git a/cmd/compose/commit.go b/cmd/compose/commit.go new file mode 100644 index 0000000000..396fac5719 --- /dev/null +++ b/cmd/compose/commit.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" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/opts" + "github.com/docker/compose/v2/pkg/api" + "github.com/spf13/cobra" +) + +type commitOptions struct { + *ProjectOptions + + service string + reference string + + pause bool + comment string + author string + changes opts.ListOpts + + index int +} + +func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { + options := commitOptions{ + ProjectOptions: p, + } + cmd := &cobra.Command{ + Use: "commit [OPTIONS] SERVICE [REPOSITORY[:TAG]]", + Short: "Create a new image from a service container's changes", + Args: cobra.RangeArgs(1, 2), + PreRunE: Adapt(func(ctx context.Context, args []string) error { + options.service = args[0] + if len(args) > 1 { + options.reference = args[1] + } + + return nil + }), + RunE: Adapt(func(ctx context.Context, args []string) error { + return runCommit(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.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit") + flags.StringVarP(&options.comment, "message", "m", "", "Commit message") + flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith ")`) + options.changes = opts.NewListOpts(nil) + flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image") + + return cmd +} + +func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service, options commitOptions) error { + projectName, err := options.toProjectName(ctx, dockerCli) + if err != nil { + return err + } + + commitOptions := api.CommitOptions{ + Service: options.service, + Reference: options.reference, + Pause: options.pause, + Comment: options.comment, + Author: options.author, + Changes: options.changes, + Index: options.index, + } + + return backend.Commit(ctx, projectName, commitOptions) +} diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 2d2d5fdd05..de46bd07eb 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -617,6 +617,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli execCommand(&opts, dockerCli, backend), attachCommand(&opts, dockerCli, backend), exportCommand(&opts, dockerCli, backend), + commitCommand(&opts, dockerCli, backend), pauseCommand(&opts, dockerCli, backend), unpauseCommand(&opts, dockerCli, backend), topCommand(&opts, dockerCli, backend), diff --git a/docs/reference/compose.md b/docs/reference/compose.md index 5a69a01b50..d625c253e6 100644 --- a/docs/reference/compose.md +++ b/docs/reference/compose.md @@ -13,6 +13,7 @@ Define and run multi-container applications with Docker |:--------------------------------|:----------------------------------------------------------------------------------------| | [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container | | [`build`](compose_build.md) | Build or rebuild services | +| [`commit`](compose_commit.md) | Create a new image from a service container's changes | | [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format | | [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem | | [`create`](compose_create.md) | Creates containers for a service | diff --git a/docs/reference/compose_commit.md b/docs/reference/compose_commit.md new file mode 100644 index 0000000000..1aad40931f --- /dev/null +++ b/docs/reference/compose_commit.md @@ -0,0 +1,19 @@ +# docker compose commit + + +Create a new image from a service container's changes + +### Options + +| Name | Type | Default | Description | +|:------------------|:---------|:--------|:-----------------------------------------------------------| +| `-a`, `--author` | `string` | | Author (e.g., "John Hannibal Smith ") | +| `-c`, `--change` | `list` | | Apply Dockerfile instruction to the created image | +| `--dry-run` | `bool` | | Execute command in dry run mode | +| `--index` | `int` | `0` | index of the container if service has multiple replicas. | +| `-m`, `--message` | `string` | | Commit message | +| `-p`, `--pause` | `bool` | `true` | Pause container during commit | + + + + diff --git a/docs/reference/docker_compose.yaml b/docs/reference/docker_compose.yaml index f59ec4a04b..1c6fb4970e 100644 --- a/docs/reference/docker_compose.yaml +++ b/docs/reference/docker_compose.yaml @@ -7,6 +7,7 @@ plink: docker.yaml cname: - docker compose attach - docker compose build + - docker compose commit - docker compose config - docker compose cp - docker compose create @@ -39,6 +40,7 @@ cname: clink: - docker_compose_attach.yaml - docker_compose_build.yaml + - docker_compose_commit.yaml - docker_compose_config.yaml - docker_compose_cp.yaml - docker_compose_create.yaml diff --git a/docs/reference/docker_compose_commit.yaml b/docs/reference/docker_compose_commit.yaml new file mode 100644 index 0000000000..95f4834a97 --- /dev/null +++ b/docs/reference/docker_compose_commit.yaml @@ -0,0 +1,76 @@ +command: docker compose commit +short: Create a new image from a service container's changes +long: Create a new image from a service container's changes +usage: docker compose commit [OPTIONS] SERVICE [REPOSITORY[:TAG]] +pname: docker compose +plink: docker_compose.yaml +options: + - option: author + shorthand: a + value_type: string + description: Author (e.g., "John Hannibal Smith ") + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false + - option: change + shorthand: c + value_type: list + description: Apply Dockerfile instruction to the created image + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false + - 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: message + shorthand: m + value_type: string + description: Commit message + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false + - option: pause + shorthand: p + value_type: bool + default_value: "true" + description: Pause container during commit + 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 f13c8b744e..ccd56b74b6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -23,6 +23,7 @@ import ( "time" "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/cli/opts" "github.com/docker/compose/v2/pkg/utils" ) @@ -92,6 +93,8 @@ type Service interface { 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 + // Create a new image from a service container's changes + Commit(ctx context.Context, projectName string, options CommitOptions) error // Generate generates a Compose Project from existing containers Generate(ctx context.Context, options GenerateOptions) (*types.Project, error) } @@ -565,6 +568,19 @@ type ExportOptions struct { Output string } +// CommitOptions group options of the Commit API +type CommitOptions struct { + Service string + Reference string + + Pause bool + Comment string + Author string + Changes opts.ListOpts + + Index int +} + type GenerateOptions struct { // ProjectName to set in the Compose file ProjectName string diff --git a/pkg/compose/commit.go b/pkg/compose/commit.go new file mode 100644 index 0000000000..8ff4e53898 --- /dev/null +++ b/pkg/compose/commit.go @@ -0,0 +1,87 @@ +/* + 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" + "strings" + + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/progress" + containerType "github.com/docker/docker/api/types/container" +) + +func (s *composeService) Commit(ctx context.Context, projectName string, options api.CommitOptions) error { + return progress.RunWithTitle(ctx, func(ctx context.Context) error { + return s.commit(ctx, projectName, options) + }, s.stdinfo(), "Committing") +} + +func (s *composeService) commit(ctx context.Context, projectName string, options api.CommitOptions) error { + projectName = strings.ToLower(projectName) + + container, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, options.Service, options.Index) + if err != nil { + return err + } + + clnt := s.dockerCli.Client() + + w := progress.ContextWriter(ctx) + + name := getCanonicalContainerName(container) + msg := fmt.Sprintf("Commit %s", name) + + w.Event(progress.Event{ + ID: name, + Text: msg, + Status: progress.Working, + StatusText: "Committing", + }) + + if s.dryRun { + w.Event(progress.Event{ + ID: name, + Text: msg, + Status: progress.Done, + StatusText: "Committed", + }) + + return nil + } + + response, err := clnt.ContainerCommit(ctx, container.ID, containerType.CommitOptions{ + Reference: options.Reference, + Comment: options.Comment, + Author: options.Author, + Changes: options.Changes.GetAll(), + Pause: options.Pause, + }) + if err != nil { + return err + } + + w.Event(progress.Event{ + ID: name, + Text: msg, + Status: progress.Done, + StatusText: fmt.Sprintf("Committed as %s", response.ID), + }) + + return nil +} diff --git a/pkg/e2e/commit_test.go b/pkg/e2e/commit_test.go new file mode 100644 index 0000000000..e3a7afa1ee --- /dev/null +++ b/pkg/e2e/commit_test.go @@ -0,0 +1,93 @@ +/* + 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 TestCommit(t *testing.T) { + const projectName = "e2e-commit-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/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service") + + c.RunDockerComposeCmd( + t, + "--project-name", + projectName, + "commit", + "-a", + "\"John Hannibal Smith \"", + "-c", + "\"ENV DEBUG=true\"", + "-m", + "\"sample commit\"", + "service", + "service:latest", + ) +} + +func TestCommitWithReplicas(t *testing.T) { + const projectName = "e2e-commit-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/commit/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas") + + c.RunDockerComposeCmd( + t, + "--project-name", + projectName, + "commit", + "-a", + "\"John Hannibal Smith \"", + "-c", + "\"ENV DEBUG=true\"", + "-m", + "\"sample commit\"", + "--index=1", + "service-with-replicas", + "service-with-replicas:1", + ) + c.RunDockerComposeCmd( + t, + "--project-name", + projectName, + "commit", + "-a", + "\"John Hannibal Smith \"", + "-c", + "\"ENV DEBUG=true\"", + "-m", + "\"sample commit\"", + "--index=2", + "service-with-replicas", + "service-with-replicas:2", + ) +} diff --git a/pkg/e2e/fixtures/commit/compose.yaml b/pkg/e2e/fixtures/commit/compose.yaml new file mode 100644 index 0000000000..28e4b15bd6 --- /dev/null +++ b/pkg/e2e/fixtures/commit/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 8bd19b9067..adf811a900 100644 --- a/pkg/mocks/mock_docker_compose_api.go +++ b/pkg/mocks/mock_docker_compose_api.go @@ -69,6 +69,20 @@ func (mr *MockServiceMockRecorder) Build(ctx, project, options any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockService)(nil).Build), ctx, project, options) } +// Commit mocks base method. +func (m *MockService) Commit(ctx context.Context, projectName string, options api.CommitOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx, projectName, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *MockServiceMockRecorder) Commit(ctx, projectName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockService)(nil).Commit), ctx, projectName, options) +} + // Copy mocks base method. func (m *MockService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error { m.ctrl.T.Helper() From 8efe30a3a6b017e35111e7a3cbeb7b9d48277f7c Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari Date: Tue, 26 Nov 2024 22:00:47 +0330 Subject: [PATCH 2/2] fix: commit tests Signed-off-by: MohammadHasan Akbari --- pkg/e2e/commit_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/e2e/commit_test.go b/pkg/e2e/commit_test.go index e3a7afa1ee..0daf130545 100644 --- a/pkg/e2e/commit_test.go +++ b/pkg/e2e/commit_test.go @@ -38,11 +38,11 @@ func TestCommit(t *testing.T) { projectName, "commit", "-a", - "\"John Hannibal Smith \"", + "John Hannibal Smith ", "-c", - "\"ENV DEBUG=true\"", + "ENV DEBUG=true", "-m", - "\"sample commit\"", + "sample commit", "service", "service:latest", ) @@ -66,11 +66,11 @@ func TestCommitWithReplicas(t *testing.T) { projectName, "commit", "-a", - "\"John Hannibal Smith \"", + "John Hannibal Smith ", "-c", - "\"ENV DEBUG=true\"", + "ENV DEBUG=true", "-m", - "\"sample commit\"", + "sample commit", "--index=1", "service-with-replicas", "service-with-replicas:1", @@ -81,11 +81,11 @@ func TestCommitWithReplicas(t *testing.T) { projectName, "commit", "-a", - "\"John Hannibal Smith \"", + "John Hannibal Smith ", "-c", - "\"ENV DEBUG=true\"", + "ENV DEBUG=true", "-m", - "\"sample commit\"", + "sample commit", "--index=2", "service-with-replicas", "service-with-replicas:2",