Skip to content

Commit 155b2f7

Browse files
committed
support nerdctl search command
support nerdctl search command Signed-off-by: ChengyuZhu6 <hudson@cyzhu.com>
1 parent 53e7b27 commit 155b2f7

File tree

7 files changed

+674
-4
lines changed

7 files changed

+674
-4
lines changed

cmd/nerdctl/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest"
4545
"github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace"
4646
"github.com/containerd/nerdctl/v2/cmd/nerdctl/network"
47+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/search"
4748
"github.com/containerd/nerdctl/v2/cmd/nerdctl/system"
4849
"github.com/containerd/nerdctl/v2/cmd/nerdctl/volume"
4950
"github.com/containerd/nerdctl/v2/pkg/config"
@@ -309,6 +310,7 @@ Config file ($NERDCTL_TOML): %s
309310
image.TagCommand(),
310311
image.RmiCommand(),
311312
image.HistoryCommand(),
313+
search.Command(),
312314
// #endregion
313315

314316
// #region System

cmd/nerdctl/search/search.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 search
18+
19+
import (
20+
"github.com/spf13/cobra"
21+
22+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
23+
"github.com/containerd/nerdctl/v2/pkg/api/types"
24+
"github.com/containerd/nerdctl/v2/pkg/cmd/search"
25+
)
26+
27+
func Command() *cobra.Command {
28+
cmd := &cobra.Command{
29+
Use: "search [OPTIONS] TERM",
30+
Short: "Search registry for images",
31+
Args: cobra.ExactArgs(1),
32+
RunE: runSearch,
33+
DisableFlagsInUseLine: true,
34+
}
35+
36+
flags := cmd.Flags()
37+
38+
flags.Bool("no-trunc", false, "Don't truncate output")
39+
flags.StringSliceP("filter", "f", nil, "Filter output based on conditions provided")
40+
flags.Int("limit", 0, "Max number of search results")
41+
flags.String("format", "", "Pretty-print search using a Go template")
42+
43+
return cmd
44+
}
45+
46+
func processSearchFlags(cmd *cobra.Command) (types.SearchOptions, error) {
47+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
48+
if err != nil {
49+
return types.SearchOptions{}, err
50+
}
51+
52+
noTrunc, err := cmd.Flags().GetBool("no-trunc")
53+
if err != nil {
54+
return types.SearchOptions{}, err
55+
}
56+
limit, err := cmd.Flags().GetInt("limit")
57+
if err != nil {
58+
return types.SearchOptions{}, err
59+
}
60+
format, err := cmd.Flags().GetString("format")
61+
if err != nil {
62+
return types.SearchOptions{}, err
63+
}
64+
filter, err := cmd.Flags().GetStringSlice("filter")
65+
if err != nil {
66+
return types.SearchOptions{}, err
67+
}
68+
69+
return types.SearchOptions{
70+
Stdout: cmd.OutOrStdout(),
71+
GOptions: globalOptions,
72+
NoTrunc: noTrunc,
73+
Limit: limit,
74+
Filters: filter,
75+
Format: format,
76+
}, nil
77+
}
78+
79+
func runSearch(cmd *cobra.Command, args []string) error {
80+
options, err := processSearchFlags(cmd)
81+
if err != nil {
82+
return err
83+
}
84+
85+
return search.Search(cmd.Context(), args[0], options)
86+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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 search
18+
19+
import (
20+
"errors"
21+
"regexp"
22+
"testing"
23+
24+
"github.com/containerd/nerdctl/mod/tigron/expect"
25+
"github.com/containerd/nerdctl/mod/tigron/test"
26+
27+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
28+
)
29+
30+
// All tests in this file are based on the output of `nerdctl search alpine`.
31+
//
32+
// Expected output format (default behavior with --limit 10):
33+
//
34+
// NAME DESCRIPTION STARS OFFICIAL
35+
// alpine A minimal Docker image based on Alpine Linux… 11437 [OK]
36+
// alpine/git A simple git container running in alpine li… 249
37+
// alpine/socat Run socat command in alpine container 115
38+
// alpine/helm Auto-trigger docker build for kubernetes hel… 69
39+
// alpine/curl 11
40+
// alpine/k8s Kubernetes toolbox for EKS (kubectl, helm, i… 64
41+
// alpine/bombardier Auto-trigger docker build for bombardier whe… 28
42+
// alpine/httpie Auto-trigger docker build for `httpie` when … 21
43+
// alpine/terragrunt Auto-trigger docker build for terragrunt whe… 18
44+
// alpine/openssl openssl 7
45+
46+
func TestSearch(t *testing.T) {
47+
testCase := nerdtest.Setup()
48+
49+
testCase.SubTests = []*test.Case{
50+
{
51+
Description: "basic-search",
52+
Command: test.Command("search", "alpine", "--limit", "5"),
53+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
54+
return &test.Expected{
55+
ExitCode: expect.ExitCodeSuccess,
56+
Output: expect.All(
57+
expect.Contains("NAME"),
58+
expect.Contains("DESCRIPTION"),
59+
expect.Contains("STARS"),
60+
expect.Contains("OFFICIAL"),
61+
expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)),
62+
expect.Contains("alpine"),
63+
expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux`)),
64+
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)),
65+
expect.Contains("[OK]"),
66+
expect.Match(regexp.MustCompile(`alpine/\w+`)),
67+
),
68+
}
69+
},
70+
},
71+
{
72+
Description: "search-library-image",
73+
Command: test.Command("search", "library/alpine", "--limit", "5"),
74+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
75+
return &test.Expected{
76+
ExitCode: expect.ExitCodeSuccess,
77+
Output: expect.All(
78+
expect.Contains("NAME"),
79+
expect.Contains("DESCRIPTION"),
80+
expect.Contains("STARS"),
81+
expect.Contains("OFFICIAL"),
82+
expect.Contains("alpine"),
83+
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)),
84+
),
85+
}
86+
},
87+
},
88+
{
89+
Description: "search-with-no-trunc",
90+
Command: test.Command("search", "alpine", "--limit", "3", "--no-trunc"),
91+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
92+
return &test.Expected{
93+
ExitCode: expect.ExitCodeSuccess,
94+
Output: expect.All(
95+
expect.Contains("NAME"),
96+
expect.Contains("DESCRIPTION"),
97+
expect.Contains("alpine"),
98+
// With --no-trunc, the full description should be visible (not truncated with …)
99+
expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!`)),
100+
),
101+
}
102+
},
103+
},
104+
{
105+
Description: "search-with-format",
106+
Command: test.Command("search", "alpine", "--limit", "2", "--format", "{{.Name}}: {{.StarCount}}"),
107+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
108+
return &test.Expected{
109+
ExitCode: expect.ExitCodeSuccess,
110+
Output: expect.All(
111+
expect.Match(regexp.MustCompile(`alpine:\s*\d+`)),
112+
expect.DoesNotContain("NAME"),
113+
expect.DoesNotContain("DESCRIPTION"),
114+
expect.DoesNotContain("OFFICIAL"),
115+
),
116+
}
117+
},
118+
},
119+
{
120+
Description: "search-output-format",
121+
Command: test.Command("search", "alpine", "--limit", "5"),
122+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
123+
return &test.Expected{
124+
ExitCode: expect.ExitCodeSuccess,
125+
Output: expect.All(
126+
expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)),
127+
expect.Match(regexp.MustCompile(`(?m)^alpine\s+.*\s+\d+\s+\[OK\]\s*$`)),
128+
expect.Match(regexp.MustCompile(`(?m)^alpine/\w+\s+.*\s+\d+\s*$`)),
129+
expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s*$`)),
130+
),
131+
}
132+
},
133+
},
134+
{
135+
Description: "search-description-formatting",
136+
Command: test.Command("search", "alpine", "--limit", "10"),
137+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
138+
return &test.Expected{
139+
ExitCode: expect.ExitCodeSuccess,
140+
Output: expect.All(
141+
expect.Match(regexp.MustCompile(`Alpine Linux…`)),
142+
expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s+`)),
143+
expect.Match(regexp.MustCompile(`(?m)^[a-z0-9/_-]+\s+.*\s+\d+`)),
144+
),
145+
}
146+
},
147+
},
148+
}
149+
150+
testCase.Run(t)
151+
}
152+
153+
func TestSearchWithFilter(t *testing.T) {
154+
testCase := nerdtest.Setup()
155+
156+
testCase.SubTests = []*test.Case{
157+
{
158+
Description: "filter-is-official-true",
159+
Command: test.Command("search", "alpine", "--filter", "is-official=true", "--limit", "5"),
160+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
161+
return &test.Expected{
162+
ExitCode: expect.ExitCodeSuccess,
163+
Output: expect.All(
164+
expect.Contains("NAME"),
165+
expect.Contains("OFFICIAL"),
166+
expect.Contains("alpine"),
167+
expect.Contains("[OK]"),
168+
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)),
169+
),
170+
}
171+
},
172+
},
173+
{
174+
Description: "filter-stars",
175+
Command: test.Command("search", "alpine", "--filter", "stars=10000"),
176+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
177+
return &test.Expected{
178+
ExitCode: expect.ExitCodeSuccess,
179+
Output: expect.All(
180+
expect.Contains("NAME"),
181+
expect.Contains("STARS"),
182+
expect.Contains("alpine"),
183+
// The official alpine image has > 10000 stars
184+
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d{4,}\s+\[OK\]`)),
185+
),
186+
}
187+
},
188+
},
189+
}
190+
191+
testCase.Run(t)
192+
}
193+
194+
func TestSearchFilterErrors(t *testing.T) {
195+
testCase := nerdtest.Setup()
196+
197+
testCase.SubTests = []*test.Case{
198+
{
199+
Description: "invalid-filter-format",
200+
Command: test.Command("search", "alpine", "--filter", "foo"),
201+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
202+
return &test.Expected{
203+
ExitCode: expect.ExitCodeGenericFail,
204+
Errors: []error{errors.New("bad format of filter (expected name=value)")},
205+
}
206+
},
207+
},
208+
{
209+
Description: "invalid-filter-key",
210+
Command: test.Command("search", "alpine", "--filter", "foo=bar"),
211+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
212+
return &test.Expected{
213+
ExitCode: expect.ExitCodeGenericFail,
214+
Errors: []error{errors.New("invalid filter 'foo'")},
215+
}
216+
},
217+
},
218+
{
219+
Description: "invalid-stars-value",
220+
Command: test.Command("search", "alpine", "--filter", "stars=abc"),
221+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
222+
return &test.Expected{
223+
ExitCode: expect.ExitCodeGenericFail,
224+
Errors: []error{errors.New("invalid filter 'stars=abc'")},
225+
}
226+
},
227+
},
228+
{
229+
Description: "invalid-is-official-value",
230+
Command: test.Command("search", "alpine", "--filter", "is-official=abc"),
231+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
232+
return &test.Expected{
233+
ExitCode: expect.ExitCodeGenericFail,
234+
Errors: []error{errors.New("invalid filter 'is-official=abc'")},
235+
}
236+
},
237+
},
238+
}
239+
240+
testCase.Run(t)
241+
}

cmd/nerdctl/search/search_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 search
18+
19+
import (
20+
"testing"
21+
22+
"github.com/containerd/nerdctl/v2/pkg/testutil"
23+
)
24+
25+
func TestMain(m *testing.M) {
26+
testutil.M(m)
27+
}

0 commit comments

Comments
 (0)