Skip to content

Commit 61eea77

Browse files
Your Nameweapons97
Your Name
authored andcommitted
Add image squash command
Signed-off-by: weapons97 <[email protected]>
1 parent 3c8411b commit 61eea77

File tree

6 files changed

+690
-0
lines changed

6 files changed

+690
-0
lines changed

cmd/nerdctl/image/image.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func Command() *cobra.Command {
4848
encryptCommand(),
4949
decryptCommand(),
5050
pruneCommand(),
51+
squashCommand(),
5152
)
5253
return cmd
5354
}

cmd/nerdctl/image/image_squash.go

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+
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 layers specified for squashing the last N (N=layer-count) must be greater than 1.")
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+
// squashCommand returns a new `squash` command to compress the number of layers of the image
37+
func squashCommand() *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+
if layerN < 1 {
60+
return options, fmt.Errorf("invalid last-n-layer: %d", layerN)
61+
}
62+
author, err := cmd.Flags().GetString("author")
63+
if err != nil {
64+
return options, err
65+
}
66+
message, err := cmd.Flags().GetString("message")
67+
if err != nil {
68+
return options, err
69+
}
70+
71+
options = types.ImageSquashOptions{
72+
GOptions: globalOptions,
73+
74+
Author: author,
75+
Message: message,
76+
77+
SourceImageRef: args[0],
78+
TargetImageName: args[1],
79+
80+
SquashLayerLastN: layerN,
81+
}
82+
return options, nil
83+
}
84+
85+
func squashAction(cmd *cobra.Command, args []string) error {
86+
options, err := processSquashCommandFlags(cmd, args)
87+
if err != nil {
88+
return err
89+
}
90+
if !options.GOptions.Experimental {
91+
return fmt.Errorf("squash is an experimental feature, please enable experimental mode")
92+
}
93+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
94+
if err != nil {
95+
return err
96+
}
97+
defer cancel()
98+
99+
return image.Squash(ctx, client, options)
100+
}
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/mod/tigron/require"
26+
"github.com/containerd/nerdctl/mod/tigron/test"
27+
28+
"github.com/containerd/nerdctl/v2/pkg/testutil"
29+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
30+
)
31+
32+
func squashIdentifierName(identifier string) string {
33+
return fmt.Sprintf("%s-squash", identifier)
34+
}
35+
36+
func secondCommitedIdentifierName(identifier string) string {
37+
return fmt.Sprintf("%s-second", identifier)
38+
}
39+
40+
func TestSquash(t *testing.T) {
41+
testCase := nerdtest.Setup()
42+
43+
testCase.SubTests = []*test.Case{
44+
{
45+
Description: "by last-n-layer",
46+
Require: require.All(
47+
require.Not(nerdtest.Docker),
48+
nerdtest.CGroup,
49+
),
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
@@ -1009,6 +1009,24 @@ Flags:
10091009
- `--platform=<PLATFORM>` : Convert content for a specific platform
10101010
- `--all-platforms` : Convert content for all platforms (default: false)
10111011

1012+
### :nerd_face: nerdctl image squash
1013+
1014+
Squash last-n-layer into a single layer.
1015+
1016+
Usage: `nerdctl image squash [OPTIONS] SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]`
1017+
1018+
Example:
1019+
1020+
```bash
1021+
nerdctl image pull example.com/foo:latest
1022+
nerdctl image squash ----last-n-layer=2 --message="generated by nerdctl squash" example.com/foo:latest example.com/foo:squashed
1023+
```
1024+
1025+
Flags:
1026+
- `-n --last-n-layer=<NUMBER>`: The number of specify squashing the last N (N=layer-count) layers
1027+
- `-m --message=<MESSAGE>`: Commit message for the squashed image
1028+
- `-a --author=<AUTHOR>`: Author of the squashed image
1029+
10121030
## Registry
10131031

10141032
### :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)