Skip to content
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
2 changes: 2 additions & 0 deletions cmd/nerdctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/network"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/search"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/system"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/volume"
"github.com/containerd/nerdctl/v2/pkg/config"
Expand Down Expand Up @@ -309,6 +310,7 @@ Config file ($NERDCTL_TOML): %s
image.TagCommand(),
image.RmiCommand(),
image.HistoryCommand(),
search.Command(),
// #endregion

// #region System
Expand Down
86 changes: 86 additions & 0 deletions cmd/nerdctl/search/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
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 search

import (
"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/cmd/search"
)

func Command() *cobra.Command {
cmd := &cobra.Command{
Use: "search [OPTIONS] TERM",
Short: "Search registry for images",
Args: cobra.ExactArgs(1),
RunE: runSearch,
DisableFlagsInUseLine: true,
}

flags := cmd.Flags()

flags.Bool("no-trunc", false, "Don't truncate output")
flags.StringSlice("filter", nil, "Filter output based on conditions provided")
flags.Int("limit", 0, "Max number of search results")
flags.String("format", "", "Pretty-print search using a Go template")

return cmd
}

func processSearchFlags(cmd *cobra.Command) (types.SearchOptions, error) {
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.SearchOptions{}, err
}

noTrunc, err := cmd.Flags().GetBool("no-trunc")
if err != nil {
return types.SearchOptions{}, err
}
limit, err := cmd.Flags().GetInt("limit")
if err != nil {
return types.SearchOptions{}, err
}
format, err := cmd.Flags().GetString("format")
if err != nil {
return types.SearchOptions{}, err
}
filter, err := cmd.Flags().GetStringSlice("filter")
if err != nil {
return types.SearchOptions{}, err
}

return types.SearchOptions{
Stdout: cmd.OutOrStdout(),
GOptions: globalOptions,
NoTrunc: noTrunc,
Limit: limit,
Filters: filter,
Format: format,
}, nil
}

func runSearch(cmd *cobra.Command, args []string) error {
options, err := processSearchFlags(cmd)
if err != nil {
return err
}

return search.Search(cmd.Context(), args[0], options)
}
142 changes: 142 additions & 0 deletions cmd/nerdctl/search/search_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
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 search

import (
"regexp"
"testing"

"github.com/containerd/nerdctl/mod/tigron/expect"
"github.com/containerd/nerdctl/mod/tigron/test"

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

// All tests in this file are based on the output of `nerdctl search alpine`.
//
// Expected output format (default behavior with --limit 10):
//
// NAME DESCRIPTION STARS OFFICIAL
// alpine A minimal Docker image based on Alpine Linux… 11437 [OK]
// alpine/git A simple git container running in alpine li… 249
// alpine/socat Run socat command in alpine container 115
// alpine/helm Auto-trigger docker build for kubernetes hel… 69
// alpine/curl 11
// alpine/k8s Kubernetes toolbox for EKS (kubectl, helm, i… 64
// alpine/bombardier Auto-trigger docker build for bombardier whe… 28
// alpine/httpie Auto-trigger docker build for `httpie` when … 21
// alpine/terragrunt Auto-trigger docker build for terragrunt whe… 18
// alpine/openssl openssl 7

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

testCase.Command = test.Command("search", "alpine", "--limit", "5")

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
expect.Contains("NAME"),
expect.Contains("DESCRIPTION"),
expect.Contains("STARS"),
expect.Contains("OFFICIAL"),
expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)),
expect.Contains("alpine"),
expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux`)),
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)),
expect.Contains("[OK]"),
expect.Match(regexp.MustCompile(`alpine/\w+`)),
))

testCase.Run(t)
}

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

testCase.Command = test.Command("search", "alpine", "--filter", "is-official=true", "--limit", "5")

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
expect.Contains("NAME"),
expect.Contains("OFFICIAL"),
expect.Contains("alpine"),
expect.Contains("[OK]"),
expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)),
))

testCase.Run(t)
}

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

testCase.Command = test.Command("search", "alpine", "--limit", "3", "--no-trunc")

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
expect.Contains("NAME"),
expect.Contains("DESCRIPTION"),
expect.Contains("alpine"),
// With --no-trunc, the full description should be visible (not truncated with …)
// The alpine description is longer than 45 chars, so without truncation
// we should see more complete text
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!`)),
))

testCase.Run(t)
}

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

testCase.Command = test.Command("search", "alpine", "--limit", "2", "--format", "{{.Name}}: {{.StarCount}}")

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
expect.Match(regexp.MustCompile(`alpine:\s*\d+`)),
expect.DoesNotContain("NAME"),
expect.DoesNotContain("DESCRIPTION"),
expect.DoesNotContain("OFFICIAL"),
))

testCase.Run(t)
}

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

testCase.Command = test.Command("search", "alpine", "--limit", "5")

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)),
expect.Match(regexp.MustCompile(`(?m)^alpine\s+.*\s+\d+\s+\[OK\]\s*$`)),
expect.Match(regexp.MustCompile(`(?m)^alpine/\w+\s+.*\s+\d+\s*$`)),
expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s*$`)),
))

testCase.Run(t)
}

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

testCase.Command = test.Command("search", "alpine", "--limit", "10")

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(
expect.Match(regexp.MustCompile(`Alpine Linux…`)),
expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s+`)),
expect.Match(regexp.MustCompile(`(?m)^[a-z0-9/_-]+\s+.*\s+\d+`)),
))

testCase.Run(t)
}
27 changes: 27 additions & 0 deletions cmd/nerdctl/search/search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
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 search

import (
"testing"

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

func TestMain(m *testing.M) {
testutil.M(m)
}
18 changes: 14 additions & 4 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
- [Registry](#registry)
- [:whale: nerdctl login](#whale-nerdctl-login)
- [:whale: nerdctl logout](#whale-nerdctl-logout)
- [:whale: nerdctl search](#whale-nerdctl-search)
- [Network management](#network-management)
- [:whale: nerdctl network create](#whale-nerdctl-network-create)
- [:whale: nerdctl network ls](#whale-nerdctl-network-ls)
Expand Down Expand Up @@ -1209,6 +1210,19 @@ Log out from a container registry

Usage: `nerdctl logout [SERVER]`

### :whale: nerdctl search

Search Docker Hub or a registry for images

Usage: `nerdctl search [OPTIONS] TERM`

Flags:

- :whale: `--limit`: Max number of search results (default: 0)
- :whale: `--no-trunc`: Don't truncate output (default: false)
- :whale: `--filter`: Filter output based on conditions provided
- :whale: `--format`: Format the output using the given Go template

## Network management

### :whale: nerdctl network create
Expand Down Expand Up @@ -1978,10 +1992,6 @@ Network management:
- `docker network connect`
- `docker network disconnect`

Registry:

- `docker search`

Compose:

- `docker-compose events|scale`
Expand Down
36 changes: 36 additions & 0 deletions pkg/api/types/search_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
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 types

import (
"io"
)

type SearchOptions struct {
Stdout io.Writer
// GOptions is the global options
GOptions GlobalCommandOptions

// NoTrunc don't truncate output
NoTrunc bool
// Limit the number of results
Limit int
// Filter output based on conditions provided, for the --filter argument
Filters []string
// Format the output using the given Go template, e.g, '{{json .}}'
Format string
}
Loading
Loading