Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image squash command #3756

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/nerdctl/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func NewImageCommand() *cobra.Command {
NewLoadCommand(),
NewSaveCommand(),
NewTagCommand(),
NewSquashCommand(),
imageRmCommand(),
newImageConvertCommand(),
newImageInspectCommand(),
Expand Down
97 changes: 97 additions & 0 deletions cmd/nerdctl/image/image_squash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Copyright The containerd 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 image

import (
"fmt"

"github.com/spf13/cobra"

"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
"github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/pkg/clientutil"
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
)

func addSquashFlags(cmd *cobra.Command) {
cmd.Flags().IntP("last-n-layer", "n", 0, "The number of specify squashing the last N (N=layer-count) layers")
cmd.Flags().StringP("author", "a", "nerdctl", `Author (e.g., "nerdctl contributor <[email protected]>")`)
cmd.Flags().StringP("message", "m", "generated by nerdctl squash", "Commit message")
}

// NewSquashCommand returns a new `squash` command to compress the number of layers of the image
func NewSquashCommand() *cobra.Command {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs docs and tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok,I will add it later.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, please check it out

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, please check it out

var squashCommand = &cobra.Command{
Use: "squash [flags] SOURCE_IMAGE TARGET_IMAGE",
Short: "Compress the number of layers of the image",
Args: helpers.IsExactArgs(2),
RunE: squashAction,
SilenceUsage: true,
SilenceErrors: true,
}
addSquashFlags(squashCommand)
return squashCommand
}

func processSquashCommandFlags(cmd *cobra.Command, args []string) (options types.ImageSquashOptions, err error) {
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return options, err
}
layerN, err := cmd.Flags().GetInt("last-n-layer")
if err != nil {
return options, err
}
Comment on lines +55 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a check to make sure layerN > 0?

author, err := cmd.Flags().GetString("author")
if err != nil {
return options, err
}
message, err := cmd.Flags().GetString("message")
if err != nil {
return options, err
}

options = types.ImageSquashOptions{
GOptions: globalOptions,

Author: author,
Message: message,

SourceImageRef: args[0],
TargetImageName: args[1],

SquashLayerLastN: layerN,
}
return options, nil
}

func squashAction(cmd *cobra.Command, args []string) error {
options, err := processSquashCommandFlags(cmd, args)
if err != nil {
return err
}
if !options.GOptions.Experimental {
return fmt.Errorf("squash is an experimental feature, please enable experimental mode")
}
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
if err != nil {
return err
}
defer cancel()

return image.Squash(ctx, client, options)
}
100 changes: 100 additions & 0 deletions cmd/nerdctl/image/image_squash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright The containerd 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 image

import (
"fmt"
"testing"

"gotest.tools/v3/assert"

"github.com/containerd/nerdctl/v2/pkg/testutil"
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
"github.com/containerd/nerdctl/v2/pkg/testutil/test"
)

func squashIdentifierName(identifier string) string {
return fmt.Sprintf("%s-squash", identifier)
}

func secondCommitedIdentifierName(identifier string) string {
return fmt.Sprintf("%s-second", identifier)
}

func TestSquash(t *testing.T) {
testCase := nerdtest.Setup()

require := test.Require(
test.Not(nerdtest.Docker),
nerdtest.CGroup,
)

testCase.SubTests = []*test.Case{
{
Description: "by last-n-layer",
Require: require,
NoParallel: true,
Cleanup: func(data test.Data, helpers test.Helpers) {
identifier := data.Identifier()
secondIdentifier := secondCommitedIdentifierName(identifier)
squashIdentifier := squashIdentifierName(identifier)
helpers.Anyhow("rm", "-f", identifier)
helpers.Anyhow("rm", "-f", secondIdentifier)
helpers.Anyhow("rm", "-f", squashIdentifier)

helpers.Anyhow("rmi", "-f", secondIdentifier)
helpers.Anyhow("rmi", "-f", identifier)
helpers.Anyhow("rmi", "-f", squashIdentifier)
helpers.Anyhow("image", "prune", "-f")
},
Setup: func(data test.Data, helpers test.Helpers) {
identifier := data.Identifier()
helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity)
helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-first-commit > /foo`)
helpers.Ensure("commit", "-c", `CMD ["cat", "/foo"]`, "-m", `first commit`, "--pause=true", identifier, identifier)
out := helpers.Capture("run", "--rm", identifier)
assert.Equal(t, out, "hello-first-commit\n")

secondIdentifier := secondCommitedIdentifierName(identifier)
helpers.Ensure("run", "-d", "--name", secondIdentifier, identifier, "sleep", nerdtest.Infinity)
helpers.Ensure("exec", secondIdentifier, "sh", "-euxc", `echo hello-second-commit > /bar && echo hello-squash-commit > /foo`)
helpers.Ensure("commit", "-c", `CMD ["cat", "/foo", "/bar"]`, "-m", `second commit`, "--pause=true", secondIdentifier, secondIdentifier)
out = helpers.Capture("run", "--rm", secondIdentifier)
assert.Equal(t, out, "hello-squash-commit\nhello-second-commit\n")

squashIdentifier := squashIdentifierName(identifier)
helpers.Ensure("image", "squash", "-n", "2", "-m", "squash commit", secondIdentifier, squashIdentifier)
out = helpers.Capture("run", "--rm", squashIdentifier)
assert.Equal(t, out, "hello-squash-commit\nhello-second-commit\n")
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
identifier := data.Identifier()

squashIdentifier := squashIdentifierName(identifier)
return helpers.Command("image", "history", "--human=true", "--format=json", squashIdentifier)
},
Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
history, err := decode(stdout)
assert.NilError(t, err, info)
assert.Equal(t, len(history), 3, info)
assert.Equal(t, history[0].Comment, "squash commit", info)
}),
},
}

testCase.Run(t)
}
18 changes: 18 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,24 @@ Flags:
- `--platform=<PLATFORM>` : Convert content for a specific platform
- `--all-platforms` : Convert content for all platforms (default: false)

### :nerd_face: nerdctl image squash

Squash last-n-layer into a single layer.

Usage: `nerdctl image squash [OPTIONS] SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]`

Example:

```bash
nerdctl image pull example.com/foo:latest
nerdctl image squash ----last-n-layer=2 --message="generated by nerdctl squash" example.com/foo:latest example.com/foo:squashed
```

Flags:
- `-n --last-n-layer=<NUMBER>`: The number of specify squashing the last N (N=layer-count) layers
- `-m --message=<MESSAGE>`: Commit message for the squashed image
- `-a --author=<AUTHOR>`: Author of the squashed image

## Registry

### :whale: nerdctl login
Expand Down
18 changes: 18 additions & 0 deletions pkg/api/types/image_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,21 @@ type SociOptions struct {
// Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.
MinLayerSize int64
}

// ImageSquashOptions specifies options for `nerdctl image squash`.
type ImageSquashOptions struct {
// GOptions is the global options
GOptions GlobalCommandOptions

// Author (e.g., "nerdctl contributor <[email protected]>")
Author string
// Commit message
Message string
// SourceImageRef is the image to be squashed
SourceImageRef string
// TargetImageName is the name of the squashed image
TargetImageName string

// SquashLayerLastN is the number of layers to squash
SquashLayerLastN int
}
Loading