Skip to content

Commit dd74594

Browse files
committed
support nerdctl search
Signed-off-by: ChengyuZhu6 <[email protected]>
1 parent d43e143 commit dd74594

File tree

6 files changed

+519
-0
lines changed

6 files changed

+519
-0
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.StringSlice("filter", 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: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
"regexp"
21+
"testing"
22+
23+
"github.com/containerd/nerdctl/mod/tigron/expect"
24+
"github.com/containerd/nerdctl/mod/tigron/test"
25+
26+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
27+
)
28+
29+
// All tests in this file are based on the output of `nerdctl search alpine`.
30+
//
31+
// Expected output format (default behavior with --limit 10):
32+
//
33+
// NAME DESCRIPTION STARS OFFICIAL
34+
// alpine A minimal Docker image based on Alpine Linux… 11437 [OK]
35+
// alpine/git A simple git container running in alpine li… 249
36+
// alpine/socat Run socat command in alpine container 115
37+
// alpine/helm Auto-trigger docker build for kubernetes hel… 69
38+
// alpine/curl 11
39+
// alpine/k8s Kubernetes toolbox for EKS (kubectl, helm, i… 64
40+
// alpine/bombardier Auto-trigger docker build for bombardier whe… 28
41+
// alpine/httpie Auto-trigger docker build for `httpie` when … 21
42+
// alpine/terragrunt Auto-trigger docker build for terragrunt whe… 18
43+
// alpine/openssl openssl 7
44+
45+
func TestSearch(t *testing.T) {
46+
testCase := nerdtest.Setup()
47+
48+
testCase.Command = test.Command("search", "alpine", "--limit", "5")
49+
50+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
51+
expect.Contains("NAME"),
52+
expect.Contains("DESCRIPTION"),
53+
expect.Contains("STARS"),
54+
expect.Contains("OFFICIAL"),
55+
expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)),
56+
expect.Contains("alpine"),
57+
expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux`)),
58+
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)),
59+
expect.Contains("[OK]"),
60+
expect.Match(regexp.MustCompile(`alpine/\w+`)),
61+
))
62+
63+
testCase.Run(t)
64+
}
65+
66+
func TestSearchWithFilter(t *testing.T) {
67+
testCase := nerdtest.Setup()
68+
69+
testCase.Command = test.Command("search", "alpine", "--filter", "is-official=true", "--limit", "5")
70+
71+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
72+
expect.Contains("NAME"),
73+
expect.Contains("OFFICIAL"),
74+
expect.Contains("alpine"),
75+
expect.Contains("[OK]"),
76+
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)),
77+
))
78+
79+
testCase.Run(t)
80+
}
81+
82+
func TestSearchWithNoTrunc(t *testing.T) {
83+
testCase := nerdtest.Setup()
84+
85+
testCase.Command = test.Command("search", "alpine", "--limit", "3", "--no-trunc")
86+
87+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
88+
expect.Contains("NAME"),
89+
expect.Contains("DESCRIPTION"),
90+
expect.Contains("alpine"),
91+
// With --no-trunc, the full description should be visible (not truncated with …)
92+
// The alpine description is longer than 45 chars, so without truncation
93+
// we should see more complete text
94+
expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux`)),
95+
))
96+
97+
testCase.Run(t)
98+
}
99+
100+
func TestSearchWithFormat(t *testing.T) {
101+
testCase := nerdtest.Setup()
102+
103+
testCase.Command = test.Command("search", "alpine", "--limit", "2", "--format", "{{.Name}}: {{.StarCount}}")
104+
105+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
106+
expect.Match(regexp.MustCompile(`alpine:\s*\d+`)),
107+
expect.DoesNotContain("NAME"),
108+
expect.DoesNotContain("DESCRIPTION"),
109+
expect.DoesNotContain("OFFICIAL"),
110+
))
111+
112+
testCase.Run(t)
113+
}
114+
115+
func TestSearchOutputFormat(t *testing.T) {
116+
testCase := nerdtest.Setup()
117+
118+
testCase.Command = test.Command("search", "alpine", "--limit", "5")
119+
120+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
121+
expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)),
122+
expect.Match(regexp.MustCompile(`(?m)^alpine\s+.*\s+\d+\s+\[OK\]\s*$`)),
123+
expect.Match(regexp.MustCompile(`(?m)^alpine/\w+\s+.*\s+\d+\s*$`)),
124+
expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s*$`)),
125+
))
126+
127+
testCase.Run(t)
128+
}
129+
130+
func TestSearchDescriptionFormatting(t *testing.T) {
131+
testCase := nerdtest.Setup()
132+
133+
testCase.Command = test.Command("search", "alpine", "--limit", "10")
134+
135+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
136+
expect.Match(regexp.MustCompile(`Alpine Linux…`)),
137+
expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s+`)),
138+
expect.Match(regexp.MustCompile(`(?m)^[a-z0-9/_-]+\s+.*\s+\d+`)),
139+
))
140+
141+
testCase.Run(t)
142+
}

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

pkg/api/types/search_types.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 types
18+
19+
import (
20+
"io"
21+
)
22+
23+
type SearchOptions struct {
24+
Stdout io.Writer
25+
// GOptions is the global options
26+
GOptions GlobalCommandOptions
27+
28+
// NoTrunc don't truncate output
29+
NoTrunc bool
30+
// Limit the number of results
31+
Limit int
32+
// Filter output based on conditions provided, for the --filter argument
33+
Filters []string
34+
// Format the output using the given Go template, e.g, '{{json .}}'
35+
Format string
36+
}

0 commit comments

Comments
 (0)