Browse Source

feat: add commit command

Signed-off-by: MohammadHasan Akbari <[email protected]>
MohammadHasan Akbari 1 year ago
parent
commit
9eaba55973

+ 93 - 0
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 <[email protected]>")`)
+	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)
+}

+ 1 - 0
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),

+ 1 - 0
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                                                        |

+ 19 - 0
docs/reference/compose_commit.md

@@ -0,0 +1,19 @@
+# docker compose commit
+
+<!---MARKER_GEN_START-->
+Create a new image from a service container's changes
+
+### Options
+
+| Name              | Type     | Default | Description                                                |
+|:------------------|:---------|:--------|:-----------------------------------------------------------|
+| `-a`, `--author`  | `string` |         | Author (e.g., "John Hannibal Smith <[email protected]>") |
+| `-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                              |
+
+
+<!---MARKER_GEN_END-->
+

+ 2 - 0
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

+ 76 - 0
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 <[email protected]>")
+      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
+

+ 16 - 0
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

+ 87 - 0
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
+}

+ 93 - 0
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 <[email protected]>\"",
+		"-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 <[email protected]>\"",
+		"-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 <[email protected]>\"",
+		"-c",
+		"\"ENV DEBUG=true\"",
+		"-m",
+		"\"sample commit\"",
+		"--index=2",
+		"service-with-replicas",
+		"service-with-replicas:2",
+	)
+}

+ 9 - 0
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

+ 14 - 0
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()