Skip to content

Commit 71fb80c

Browse files
Your Nameweapons97
Your Name
authored andcommitted
Add image squash command
Signed-off-by: weapons97 <[email protected]>
1 parent f917e5c commit 71fb80c

File tree

6 files changed

+684
-0
lines changed

6 files changed

+684
-0
lines changed

cmd/nerdctl/image/image.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func NewImageCommand() *cobra.Command {
4242
NewLoadCommand(),
4343
NewSaveCommand(),
4444
NewTagCommand(),
45+
NewSquashCommand(),
4546
imageRmCommand(),
4647
newImageConvertCommand(),
4748
newImageInspectCommand(),

cmd/nerdctl/image/image_squash.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package image
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/spf13/cobra"
23+
24+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
25+
"github.com/containerd/nerdctl/v2/pkg/api/types"
26+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
27+
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
28+
)
29+
30+
func addSquashFlags(cmd *cobra.Command) {
31+
cmd.Flags().IntP("last-n-layer", "n", 0, "The number of specify squashing the last N (N=layer-count) layers")
32+
cmd.Flags().StringP("author", "a", "nerdctl", `Author (e.g., "nerdctl contributor <[email protected]>")`)
33+
cmd.Flags().StringP("message", "m", "generated by nerdctl squash", "Commit message")
34+
}
35+
36+
// NewSquashCommand returns a new `squash` command to compress the number of layers of the image
37+
func NewSquashCommand() *cobra.Command {
38+
var squashCommand = &cobra.Command{
39+
Use: "squash [flags] SOURCE_IMAGE TARGET_IMAGE",
40+
Short: "Compress the number of layers of the image",
41+
Args: helpers.IsExactArgs(2),
42+
RunE: squashAction,
43+
SilenceUsage: true,
44+
SilenceErrors: true,
45+
}
46+
addSquashFlags(squashCommand)
47+
return squashCommand
48+
}
49+
50+
func processSquashCommandFlags(cmd *cobra.Command, args []string) (options types.ImageSquashOptions, err error) {
51+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
52+
if err != nil {
53+
return options, err
54+
}
55+
layerN, err := cmd.Flags().GetInt("last-n-layer")
56+
if err != nil {
57+
return options, err
58+
}
59+
author, err := cmd.Flags().GetString("author")
60+
if err != nil {
61+
return options, err
62+
}
63+
message, err := cmd.Flags().GetString("message")
64+
if err != nil {
65+
return options, err
66+
}
67+
68+
options = types.ImageSquashOptions{
69+
GOptions: globalOptions,
70+
71+
Author: author,
72+
Message: message,
73+
74+
SourceImageRef: args[0],
75+
TargetImageName: args[1],
76+
77+
SquashLayerLastN: layerN,
78+
}
79+
return options, nil
80+
}
81+
82+
func squashAction(cmd *cobra.Command, args []string) error {
83+
options, err := processSquashCommandFlags(cmd, args)
84+
if err != nil {
85+
return err
86+
}
87+
if !options.GOptions.Experimental {
88+
return fmt.Errorf("squash is an experimental feature, please enable experimental mode")
89+
}
90+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
91+
if err != nil {
92+
return err
93+
}
94+
defer cancel()
95+
96+
return image.Squash(ctx, client, options)
97+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package image
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"gotest.tools/v3/assert"
24+
25+
"github.com/containerd/nerdctl/v2/pkg/testutil"
26+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
27+
"github.com/containerd/nerdctl/v2/pkg/testutil/test"
28+
)
29+
30+
func squashIdentifierName(identifier string) string {
31+
return fmt.Sprintf("%s-squash", identifier)
32+
}
33+
34+
func secondCommitedIdentifierName(identifier string) string {
35+
return fmt.Sprintf("%s-second", identifier)
36+
}
37+
38+
func TestSquash(t *testing.T) {
39+
testCase := nerdtest.Setup()
40+
41+
require := test.Require(
42+
test.Not(nerdtest.Docker),
43+
nerdtest.CGroup,
44+
)
45+
46+
testCase.SubTests = []*test.Case{
47+
{
48+
Description: "by last-n-layer",
49+
Require: require,
50+
NoParallel: true,
51+
Cleanup: func(data test.Data, helpers test.Helpers) {
52+
identifier := data.Identifier()
53+
secondIdentifier := secondCommitedIdentifierName(identifier)
54+
squashIdentifier := squashIdentifierName(identifier)
55+
helpers.Anyhow("rm", "-f", identifier)
56+
helpers.Anyhow("rm", "-f", secondIdentifier)
57+
helpers.Anyhow("rm", "-f", squashIdentifier)
58+
59+
helpers.Anyhow("rmi", "-f", secondIdentifier)
60+
helpers.Anyhow("rmi", "-f", identifier)
61+
helpers.Anyhow("rmi", "-f", squashIdentifier)
62+
helpers.Anyhow("image", "prune", "-f")
63+
},
64+
Setup: func(data test.Data, helpers test.Helpers) {
65+
identifier := data.Identifier()
66+
helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity)
67+
helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-first-commit > /foo`)
68+
helpers.Ensure("commit", "-c", `CMD ["cat", "/foo"]`, "-m", `first commit`, "--pause=true", identifier, identifier)
69+
out := helpers.Capture("run", "--rm", identifier)
70+
assert.Equal(t, out, "hello-first-commit\n")
71+
72+
secondIdentifier := secondCommitedIdentifierName(identifier)
73+
helpers.Ensure("run", "-d", "--name", secondIdentifier, identifier, "sleep", nerdtest.Infinity)
74+
helpers.Ensure("exec", secondIdentifier, "sh", "-euxc", `echo hello-second-commit > /bar && echo hello-squash-commit > /foo`)
75+
helpers.Ensure("commit", "-c", `CMD ["cat", "/foo", "/bar"]`, "-m", `second commit`, "--pause=true", secondIdentifier, secondIdentifier)
76+
out = helpers.Capture("run", "--rm", secondIdentifier)
77+
assert.Equal(t, out, "hello-squash-commit\nhello-second-commit\n")
78+
79+
squashIdentifier := squashIdentifierName(identifier)
80+
helpers.Ensure("image", "squash", "-n", "2", "-m", "squash commit", secondIdentifier, squashIdentifier)
81+
out = helpers.Capture("run", "--rm", squashIdentifier)
82+
assert.Equal(t, out, "hello-squash-commit\nhello-second-commit\n")
83+
},
84+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
85+
identifier := data.Identifier()
86+
87+
squashIdentifier := squashIdentifierName(identifier)
88+
return helpers.Command("image", "history", "--human=true", "--format=json", squashIdentifier)
89+
},
90+
Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
91+
history, err := decode(stdout)
92+
assert.NilError(t, err, info)
93+
assert.Equal(t, len(history), 3, info)
94+
assert.Equal(t, history[0].Comment, "squash commit", info)
95+
}),
96+
},
97+
}
98+
99+
testCase.Run(t)
100+
}

docs/command-reference.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,24 @@ Flags:
994994
- `--platform=<PLATFORM>` : Convert content for a specific platform
995995
- `--all-platforms` : Convert content for all platforms (default: false)
996996

997+
### :nerd_face: nerdctl image squash
998+
999+
Squash last-n-layer into a single layer.
1000+
1001+
Usage: `nerdctl image squash [OPTIONS] SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]`
1002+
1003+
Example:
1004+
1005+
```bash
1006+
nerdctl image pull example.com/foo:latest
1007+
nerdctl image squash ----last-n-layer=2 --message="generated by nerdctl squash" example.com/foo:latest example.com/foo:squashed
1008+
```
1009+
1010+
Flags:
1011+
- `-n --last-n-layer=<NUMBER>`: The number of specify squashing the last N (N=layer-count) layers
1012+
- `-m --message=<MESSAGE>`: Commit message for the squashed image
1013+
- `-a --author=<AUTHOR>`: Author of the squashed image
1014+
9971015
## Registry
9981016

9991017
### :whale: nerdctl login

pkg/api/types/image_types.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,21 @@ type SociOptions struct {
290290
// Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.
291291
MinLayerSize int64
292292
}
293+
294+
// ImageSquashOptions specifies options for `nerdctl image squash`.
295+
type ImageSquashOptions struct {
296+
// GOptions is the global options
297+
GOptions GlobalCommandOptions
298+
299+
// Author (e.g., "nerdctl contributor <[email protected]>")
300+
Author string
301+
// Commit message
302+
Message string
303+
// SourceImageRef is the image to be squashed
304+
SourceImageRef string
305+
// TargetImageName is the name of the squashed image
306+
TargetImageName string
307+
308+
// SquashLayerLastN is the number of layers to squash
309+
SquashLayerLastN int
310+
}

0 commit comments

Comments
 (0)