Merge pull request #27333 from Honny1/search-tests-without-net

Eliminate network dependencies in `podman search` e2e tests with mock registry
This commit is contained in:
openshift-merge-bot[bot]
2025-10-31 19:33:10 +00:00
committed by GitHub
2 changed files with 796 additions and 329 deletions

View File

@@ -0,0 +1,562 @@
//go:build linux || freebsd
package integration
import (
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
. "github.com/onsi/ginkgo/v2" //nolint:staticcheck
. "github.com/onsi/gomega" //nolint:staticcheck
)
const (
contentTypeJSON = "application/json"
)
var searchResults = map[string]any{
"alpine": map[string]any{
"query": "alpine",
"num_results": 25,
"num_pages": 2,
"page": 1,
"page_size": 25,
"results": []map[string]any{
{
"name": "cilium/alpine-curl",
"description": "",
"is_public": true,
"href": "/repository/cilium/alpine-curl",
},
{
"name": "libpod/alpine",
"description": "This image is used for testing purposes only. Do NOT use it in production!",
"is_public": true,
"href": "/repository/libpod/alpine",
"stars": 11,
"official": true,
},
{
"name": "openshifttest/alpine",
"description": nil,
"is_public": true,
"href": "/repository/openshifttest/alpine",
"stars": 5,
"official": false,
"is_automated": true,
},
{
"name": "openshifttest/base-alpine",
"description": nil,
"is_public": true,
"href": "/repository/openshifttest/base-alpine",
},
{
"name": "astronomer/ap-alpine",
"description": "",
"is_public": true,
"href": "/repository/astronomer/ap-alpine",
},
{
"name": "almworks/alpine-curl",
"description": "",
"is_public": true,
"href": "/repository/almworks/alpine-curl",
},
{
"name": "jitesoft/alpine",
"description": "# Alpine linux",
"is_public": true,
"href": "/repository/jitesoft/alpine",
},
{
"name": "dougbtv/alpine",
"description": nil,
"is_public": true,
"href": "/repository/dougbtv/alpine",
},
{
"name": "tccr/alpine",
"description": nil,
"is_public": true,
"href": "/repository/tccr/alpine",
},
{
"name": "aptible/alpine",
"description": "Alpine base image, borrowed from gliderlabs/alpine",
"is_public": true,
"href": "/repository/aptible/alpine",
},
{
"name": "openshifttest/nginx-alpine",
"description": nil,
"is_public": true,
"href": "/repository/openshifttest/nginx-alpine",
},
{
"name": "wire/alpine-git",
"description": "",
"is_public": true,
"href": "/repository/wire/alpine-git",
},
{
"name": "ditto/alpine-non-root",
"description": "",
"is_public": true,
"href": "/repository/ditto/alpine-non-root",
},
{
"name": "kubevirt/alpine-ext-kernel-boot-demo",
"description": "",
"is_public": true,
"href": "/repository/kubevirt/alpine-ext-kernel-boot-demo",
},
{
"name": "ansible/alpine321-test-container",
"description": "",
"is_public": true,
"href": "/repository/ansible/alpine321-test-container",
},
{
"name": "crio/alpine",
"description": nil,
"is_public": true,
"href": "/repository/crio/alpine",
},
{
"name": "ansible/alpine-test-container",
"description": "",
"is_public": true,
"href": "/repository/ansible/alpine-test-container",
},
{
"name": "ansible/alpine322-test-container",
"description": "",
"is_public": true,
"href": "/repository/ansible/alpine322-test-container",
},
{
"name": "bedrock/alpine",
"description": "",
"is_public": true,
"href": "/repository/bedrock/alpine",
},
{
"name": "ansible/alpine3-test-container",
"description": "",
"is_public": true,
"href": "/repository/ansible/alpine3-test-container",
},
{
"name": "openshift-psap-qe/nginx-alpine",
"description": nil,
"is_public": true,
"href": "/repository/openshift-psap-qe/nginx-alpine",
},
{
"name": "startx/alpine",
"description": "",
"is_public": true,
"href": "/repository/startx/alpine",
},
{
"name": "pcc3202/alpine_multi",
"description": "",
"is_public": true,
"href": "/repository/pcc3202/alpine_multi",
},
{
"name": "nvlab/alpine",
"description": nil,
"is_public": true,
"href": "/repository/nvlab/alpine",
},
{
"name": "kubevirt/alpine-container-disk-demo",
"description": "Part of kubevirt/kubevirt artifacts",
"is_public": true,
"href": "/repository/kubevirt/alpine-container-disk-demo",
},
},
},
"busybox": map[string]any{
"num_results": 2,
"query": "busybox",
"results": []map[string]any{
{
"name": "busybox",
"description": "Busybox base image",
"star_count": 80,
"is_official": true,
"is_automated": false,
},
{
"name": "progrium/busybox",
"description": "Custom busybox build",
"star_count": 15,
"is_official": false,
"is_automated": true,
},
},
},
"skopeo/stable:latest": map[string]any{
"query": "skopeo/stable:latest",
"num_results": 3,
"num_pages": 1,
"page": 1,
"page_size": 25,
"results": []map[string]any{
{
"name": "skopeo/stable",
"description": "Stable Skopeo Image",
"is_public": true,
"href": "/repository/skopeo/stable",
},
{
"name": "skopeo/testing",
"description": "Testing Skopeo Image",
"is_public": true,
"href": "/repository/skopeo/testing",
},
{
"name": "skopeo/upstream",
"description": "Upstream Skopeo Image",
"is_public": true,
"href": "/repository/skopeo/upstream",
},
},
},
"podman/stable": map[string]any{
"query": "podman/stable",
"num_results": 3,
"num_pages": 1,
"page": 1,
"page_size": 25,
"results": []map[string]any{
{
"name": "podman/stable",
"description": "Stable Podman Image",
"is_public": true,
"href": "/repository/podman/stable",
},
{
"name": "podman/testing",
"description": "Testing Podman Image",
"is_public": true,
"href": "/repository/podman/testing",
},
{
"name": "podman/upstream",
"description": "Upstream Podman Image",
"is_public": true,
"href": "/repository/podman/upstream",
},
},
},
"testdigest_v2s1": map[string]any{
"query": "testdigest_v2s1",
"num_results": 2,
"num_pages": 1,
"page": 1,
"page_size": 25,
"results": []map[string]any{
{
"name": "libpod/testdigest_v2s1",
"description": "Test image used by buildah regression tests",
"is_public": true,
"href": "/repository/libpod/testdigest_v2s1",
},
{
"name": "libpod/testdigest_v2s1_with_dups",
"description": "This is a specially crafted test-only image used in buildah CI and gating tests.",
"is_public": true,
"href": "/repository/libpod/testdigest_v2s1_with_dups",
},
},
},
"testdigest_v2s2": map[string]any{
"query": "testdigest_v2s2",
"num_results": 1,
"num_pages": 1,
"page": 1,
"page_size": 25,
"results": []map[string]any{
{
"name": "libpod/testdigest_v2s2",
"description": "This is a specially crafted test-only image used in buildah CI and gating tests.",
"is_public": true,
"href": "/repository/libpod/testdigest_v2s2",
},
},
},
}
// Mock repository tag data - simplified to just store tag lists
var mockRepoTags = map[string][]string{
"libpod/alpine": {"3.10.2", "3.2", "latest", "withbogusseccomp", "withseccomp"},
"podman/stable": {
"latest", "v1.4.2", "v1.4.4", "v1.5.0", "v1.5.1", "v1.6", "v1.6.2",
"v1.9.0", "v1.9.1", "v2.0.2", "v2.0.6", "v2.1.1", "v2.2.1", "v3",
"v3.1.2", "v3.2.0", "v3.2.1", "v3.2.2", "v3.2.3", "v3.3.0", "v3.3.1",
"v3.4", "v3.4.0", "v3.4.1", "v3.4.2", "v3.4.4", "v3.4.7", "v4",
"v4.1", "v4.1.0", "v4.1.1", "v4.2", "v4.2.0", "v4.2.1", "v4.3",
"v4.3.0", "v4.3.1", "v4.4", "v4.4.1", "v4.4.2", "v4.4.4", "v4.5",
"v4.5.0", "v4.5.1", "v4.6", "v4.6.1", "v4.6.2", "v4.7", "v4.7.0",
"v4.7.2", "v4.8", "v4.8.0", "v4.8.1", "v4.8.2", "v4.8.3", "v4.9",
"v4.9.0", "v4.9.3", "v4.9.4", "v4.9.4-immutable", "v4.9-immutable",
"v4-immutable", "v5", "v5.0", "v5.0.1", "v5.0.1-immutable", "v5.0.2",
"v5.0.2-immutable", "v5.0.3", "v5.0.3-immutable", "v5.0-immutable",
"v5.1", "v5.1.0", "v5.1.0-immutable", "v5.1.1", "v5.1.1-immutable",
"v5.1.2", "v5.1.2-immutable", "v5.1-immutable", "v5.2", "v5.2.0",
"v5.2.0-immutable", "v5.2.1", "v5.2.1-immutable", "v5.2.2",
"v5.2.2-immutable", "v5.2.3", "v5.2.3-immutable", "v5.2.5",
"v5.2.5-immutable", "v5.2-immutable", "v5.3", "v5.3.0",
"v5.3.0-immutable", "v5.3.1", "v5.3.1-immutable", "v5.3.2",
"v5.3.2-immutable", "v5.3-immutable", "v5.4",
},
}
// Pagination tags for podman/stable (returned after v5.4 in pagination requests)
// This simulates the specific test case where limit=100 and last=v5.4
var podmanStablePaginatedTags = []string{
"v5.4.0", "v5.4.0-immutable", "v5.4.1", "v5.4.1-immutable", "v5.4.2",
"v5.4.2-immutable", "v5.4-immutable", "v5.5", "v5.5.0", "v5.5.0-immutable",
"v5.5.1", "v5.5.1-immutable", "v5.5.2", "v5.5.2-immutable", "v5.5-immutable",
"v5.6", "v5.6.0", "v5.6.0-immutable", "v5.6.1", "v5.6.1-immutable", "v5.6.2",
"v5.6.2-immutable", "v5.6-immutable", "v5-immutable",
}
func writeJSONResponse(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", contentTypeJSON)
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
func handleV1Search(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if decodedQuery, err := url.QueryUnescape(query); err == nil {
query = decodedQuery
}
limitStr := r.URL.Query().Get("n")
limitNum := -1
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
limitNum = limit
} else if err != nil {
http.Error(w, "Invalid limit parameter", http.StatusBadRequest)
return
}
results := searchForResults(query)
if results != nil {
response := applyLimitToResults(results, limitNum)
writeJSONResponse(w, response)
} else {
defaultResponse := map[string]any{
"num_results": 0,
"query": query,
"results": []any{},
}
writeJSONResponse(w, defaultResponse)
}
}
func searchForResults(query string) map[string]any {
regexPattern := query
if strings.Contains(query, "*") {
regexPattern = strings.ReplaceAll(query, "*", ".*")
}
for key, value := range searchResults {
match, _ := regexp.MatchString(regexPattern, key)
if match {
return value.(map[string]any)
}
}
return nil
}
func applyLimitToResults(results map[string]any, limitNum int) map[string]any {
originalBytes, err := json.Marshal(results)
if err != nil {
return results
}
var resultsCopy map[string]any
if err := json.Unmarshal(originalBytes, &resultsCopy); err != nil {
return results
}
if limitNum > 0 {
if resultsArray, ok := resultsCopy["results"].([]any); ok {
actualLimit := limitNum
if len(resultsArray) < limitNum {
actualLimit = len(resultsArray)
}
resultsCopy["results"] = resultsArray[:actualLimit]
resultsCopy["num_results"] = actualLimit
}
}
return resultsCopy
}
func handleV2(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/_catalog" {
handleCatalog(w, r)
return
}
if strings.HasSuffix(r.URL.Path, "/tags/list") {
handleTagsList(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
}
func parseRepositoryPath(path string) (string, bool) {
pathParts := strings.Split(strings.TrimPrefix(path, "/v2/"), "/")
if len(pathParts) < 2 {
return "", false
}
if pathParts[len(pathParts)-1] == "list" && pathParts[len(pathParts)-2] == "tags" {
repoName := strings.Join(pathParts[:len(pathParts)-2], "/")
return repoName, true
}
return "", false
}
func handleTagsList(w http.ResponseWriter, r *http.Request) {
repoName, isValidPath := parseRepositoryPath(r.URL.Path)
if !isValidPath {
http.Error(w, "Invalid tags list path", http.StatusBadRequest)
return
}
allTags, exists := mockRepoTags[repoName]
if !exists {
http.Error(w, fmt.Sprintf("repository %s not found", repoName), http.StatusNotFound)
return
}
query := r.URL.Query()
limit := -1
last := query.Get("last")
if limitStr := query.Get("n"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
}
paginatedTags := applyPagination(allTags, limit, last, repoName)
response := map[string]any{
"name": repoName,
"tags": paginatedTags,
}
writeJSONResponse(w, response)
}
func applyPagination(allTags []string, limit int, last string, repoName string) []string {
if repoName == "podman/stable" && limit == 100 && last == "v5.4" {
return podmanStablePaginatedTags
}
if limit <= 0 && last == "" {
return allTags
}
startIndex := 0
if last != "" {
for i, tag := range allTags {
if tag == last {
startIndex = i + 1
break
}
}
}
if limit > 0 {
endIndex := startIndex + limit
if endIndex > len(allTags) {
endIndex = len(allTags)
}
return allTags[startIndex:endIndex]
}
return allTags[startIndex:]
}
func handleCatalog(w http.ResponseWriter, _ *http.Request) {
repositories := make([]string, 0, len(mockRepoTags))
for repoName := range mockRepoTags {
repositories = append(repositories, repoName)
}
response := map[string]any{
"repositories": repositories,
}
writeJSONResponse(w, response)
}
// CreateMockRegistryServer creates and starts a mock Docker registry server
// Returns: server address, server instance, error channel, and logged requests slice
func CreateMockRegistryServer() (string, *http.Server, chan error) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
Expect(err).ToNot(HaveOccurred())
serverAddr := listener.Addr().String()
mux := http.NewServeMux()
mux.HandleFunc("/v1/search", func(w http.ResponseWriter, r *http.Request) {
handleV1Search(w, r)
})
mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) {
handleV2(w, r)
})
srv := &http.Server{
Handler: mux,
ErrorLog: log.New(io.Discard, "", 0),
}
serverErr := make(chan error, 1)
go func() {
defer GinkgoRecover()
serverErr <- srv.Serve(listener)
}()
Eventually(func() error {
resp, err := http.Get("http://" + serverAddr + "/v2/")
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("server not ready, status: %d", resp.StatusCode)
}
return nil
}, "5s", "100ms").Should(Succeed())
return serverAddr, srv, serverErr
}
func CloseMockRegistryServer(srv *http.Server, serverErr chan error) {
srv.Close()
Expect(<-serverErr).To(Equal(http.ErrServerClosed))
}

View File

@@ -6,6 +6,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"text/template"
@@ -43,327 +44,274 @@ registries = ['{{.Host}}:{{.Port}}']
registries = []`
registryFileBadTmpl := template.Must(template.New("registryFileBad").Parse(badRegFileContents))
It("podman search", func() {
search := podmanTest.Podman([]string{"search", "alpine"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 1))
Expect(search.OutputToString()).To(ContainSubstring("alpine"))
})
It("podman search single registry flag", func() {
search := podmanTest.Podman([]string{"search", "quay.io/skopeo/stable:latest"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToString()).To(ContainSubstring("quay.io/skopeo/stable"))
})
It("podman search image with description", func() {
search := podmanTest.Podman([]string{"search", "quay.io/podman/stable"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
output := string(search.Out.Contents())
Expect(output).To(MatchRegexp(`(?m)NAME\s+DESCRIPTION$`))
Expect(output).To(MatchRegexp(`(?m)quay.io/podman/stable\s+.*PODMAN logo`))
})
It("podman search image with --compatible", func() {
search := podmanTest.Podman([]string{"search", "--compatible", "quay.io/podman/stable"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
output := string(search.Out.Contents())
Expect(output).To(MatchRegexp(`(?m)NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL\s+AUTOMATED$`))
})
It("podman search format flag", func() {
search := podmanTest.Podman([]string{"search", "--format", "table {{.Index}} {{.Name}}", "testdigest_v2s2"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 1))
Expect(search.OutputToString()).To(ContainSubstring("quay.io/libpod/testdigest_v2s2"))
})
It("podman search format json", func() {
search := podmanTest.Podman([]string{"search", "--format", "json", "testdigest_v2s1"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToString()).To(BeValidJSON())
Expect(search.OutputToString()).To(ContainSubstring("quay.io/libpod/testdigest_v2s1"))
Expect(search.OutputToString()).To(ContainSubstring("Test image used by buildah regression tests"))
// Test for https://github.com/containers/podman/issues/11894
contents := make([]entities.ImageSearchReport, 0)
err := json.Unmarshal(search.Out.Contents(), &contents)
Expect(err).ToNot(HaveOccurred())
Expect(contents).ToNot(BeEmpty(), "No results from image search")
for _, element := range contents {
Expect(element.Description).ToNot(HaveSuffix("..."))
}
})
It("podman search format json list tags", func() {
search := podmanTest.Podman([]string{"search", "--list-tags", "--format", "json", ALPINE})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToString()).To(BeValidJSON())
Expect(search.OutputToString()).To(ContainSubstring("quay.io/libpod/alpine"))
Expect(search.OutputToString()).To(ContainSubstring("3.10.2"))
Expect(search.OutputToString()).To(ContainSubstring("3.2"))
})
// Test for https://github.com/containers/podman/issues/11894
It("podman search no-trunc=false flag", func() {
search := podmanTest.Podman([]string{"search", "--no-trunc=false", "alpine", "--format={{.Description}}"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
for _, line := range search.OutputToStringArray() {
if len(line) > 44 {
Expect(line).To(HaveSuffix("..."), line+" should have been truncated")
}
}
})
It("podman search limit flag", func() {
search := podmanTest.Podman([]string{"search", "quay.io/alpine"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 10))
search = podmanTest.Podman([]string{"search", "--limit", "3", "quay.io/alpine"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToStringArray()).To(HaveLen(4))
search = podmanTest.Podman([]string{"search", "--limit", "30", "quay.io/alpine"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToStringArray()).To(HaveLen(31))
})
It("podman search with filter stars", func() {
search := podmanTest.Podman([]string{"search", "--filter", "stars=10", "--format", "{{.Stars}}", "alpine"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
output := search.OutputToStringArray()
for i := range output {
Expect(strconv.Atoi(output[i])).To(BeNumerically(">=", 10))
}
})
It("podman search with filter is-official", func() {
search := podmanTest.Podman([]string{"search", "--filter", "is-official", "--format", "{{.Official}}", "alpine"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
output := search.OutputToStringArray()
for i := range output {
Expect(output[i]).To(Equal("[OK]"))
}
})
It("podman search with filter is-automated", func() {
search := podmanTest.Podman([]string{"search", "--filter", "is-automated=false", "--format", "{{.Automated}}", "alpine"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
output := search.OutputToStringArray()
for i := range output {
Expect(output[i]).To(Equal(""))
}
})
It("podman search format list tags with custom", func() {
search := podmanTest.Podman([]string{"search", "--list-tags", "--format", "{{.Name}}", "--limit", "1", ALPINE})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToString()).To(Equal("quay.io/libpod/alpine"))
})
It("podman search attempts HTTP if tls-verify flag is set false", func() {
mockFakeRegistryServerAsContainer := func(name string) endpoint {
if podmanTest.Host.Arch == "ppc64le" {
Skip("No registry image for ppc64le")
}
port := GetPort()
fakereg := podmanTest.Podman([]string{"run", "-d", "--name", "registry",
fakereg := podmanTest.Podman([]string{"run", "-d", "--name", name,
"-p", fmt.Sprintf("%d:5000", port),
REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"})
fakereg.WaitWithDefaultTimeout()
Expect(fakereg).Should(ExitCleanly())
if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) {
if !WaitContainerReady(podmanTest, name, "listening on", 20, 1) {
Fail("Cannot start docker registry on port %s", port)
}
ep := endpoint{Port: strconv.Itoa(port), Host: "localhost"}
search := podmanTest.Podman([]string{"search",
fmt.Sprintf("%s/fake/image:andtag", ep.Address()), "--tls-verify=false"})
search.WaitWithDefaultTimeout()
return ep
}
// if this test succeeded, there will be no output (there is no entry named fake/image:andtag in an empty registry)
// and the exit code will be 0
Expect(search).Should(ExitCleanly())
Expect(search.OutputToString()).Should(BeEmpty())
})
It("podman search in local registry", func() {
if podmanTest.Host.Arch == "ppc64le" {
Skip("No registry image for ppc64le")
}
port := GetPort()
registry := podmanTest.Podman([]string{"run", "-d", "--name", "registry3",
"-p", fmt.Sprintf("%d:5000", port), REGISTRY_IMAGE,
"/entrypoint.sh", "/etc/docker/registry/config.yml"})
registry.WaitWithDefaultTimeout()
Expect(registry).Should(ExitCleanly())
if !WaitContainerReady(podmanTest, "registry3", "listening on", 20, 1) {
Fail("Cannot start docker registry on port %s", port)
}
ep := endpoint{Port: strconv.Itoa(port), Host: "localhost"}
pushAlpineImageIntoMockRegistry := func(ep endpoint) string {
err = podmanTest.RestoreArtifact(ALPINE)
Expect(err).ToNot(HaveOccurred())
image := fmt.Sprintf("%s/my-alpine", ep.Address())
push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, image})
push.WaitWithDefaultTimeout()
Expect(push).Should(ExitCleanly())
search := podmanTest.Podman([]string{"search", image, "--tls-verify=false"})
search.WaitWithDefaultTimeout()
podmanTest.PodmanExitCleanly("push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, image)
return image
}
Expect(search).Should(ExitCleanly())
Expect(search.OutputToString()).ShouldNot(BeEmpty())
Context("podman search with mock registry", func() {
var registryAddress string
var srv *http.Server
var serverErr chan error
BeforeEach(func() {
registryAddress, srv, serverErr = CreateMockRegistryServer()
})
AfterEach(func() {
CloseMockRegistryServer(srv, serverErr)
})
It("podman search", func() {
search := podmanTest.PodmanExitCleanly("search", "--tls-verify=false", registryAddress+"/alpine")
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 1))
Expect(search.OutputToString()).To(ContainSubstring("alpine"))
})
It("podman search single registry flag", func() {
search := podmanTest.PodmanExitCleanly("search", "--tls-verify=false", registryAddress+"/skopeo/stable:latest")
Expect(search.OutputToString()).To(ContainSubstring(registryAddress + "/skopeo/stable"))
})
It("podman search image with description", func() {
search := podmanTest.PodmanExitCleanly("search", "--tls-verify=false", registryAddress+"/podman/stable")
output := string(search.Out.Contents())
Expect(output).To(MatchRegexp(`(?m)NAME\s+DESCRIPTION$`))
Expect(output).To(MatchRegexp(`(?m)/podman/stable\s+.*Podman Image`))
})
It("podman search image with --compatible", func() {
search := podmanTest.PodmanExitCleanly("search", "--compatible", "--tls-verify=false", registryAddress+"/podman/stable")
output := string(search.Out.Contents())
Expect(output).To(MatchRegexp(`(?m)NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL\s+AUTOMATED$`))
})
It("podman search format flag", func() {
search := podmanTest.PodmanExitCleanly("search", "--format", "table {{.Index}} {{.Name}}", "--tls-verify=false", registryAddress+"/testdigest_v2s2")
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 1))
Expect(search.OutputToString()).To(ContainSubstring(registryAddress + "/libpod/testdigest_v2s2"))
})
It("podman search format json", func() {
search := podmanTest.PodmanExitCleanly("search", "--format", "json", "--tls-verify=false", registryAddress+"/testdigest_v2s1")
Expect(search.OutputToString()).To(BeValidJSON())
Expect(search.OutputToString()).To(ContainSubstring(registryAddress + "/libpod/testdigest_v2s1"))
Expect(search.OutputToString()).To(ContainSubstring("Test image used by buildah regression tests"))
// Test for https://github.com/containers/podman/issues/11894
contents := make([]entities.ImageSearchReport, 0)
err := json.Unmarshal(search.Out.Contents(), &contents)
Expect(err).ToNot(HaveOccurred())
Expect(contents).ToNot(BeEmpty(), "No results from image search")
for _, element := range contents {
Expect(element.Description).ToNot(HaveSuffix("..."))
}
})
It("podman search format json list tags", func() {
search := podmanTest.PodmanExitCleanly("search", "--list-tags", "--format", "json", "--tls-verify=false", registryAddress+"/libpod/alpine:latest")
Expect(search.OutputToString()).To(BeValidJSON())
Expect(search.OutputToString()).To(ContainSubstring(registryAddress + "/libpod/alpine"))
Expect(search.OutputToString()).To(ContainSubstring("3.10.2"))
Expect(search.OutputToString()).To(ContainSubstring("3.2"))
})
// Test for https://github.com/containers/podman/issues/11894
It("podman search no-trunc=false flag", func() {
search := podmanTest.PodmanExitCleanly("search", "--no-trunc=false", "--tls-verify=false", registryAddress+"/alpine", "--format={{.Description}}")
for _, line := range search.OutputToStringArray() {
if len(line) > 44 {
Expect(line).To(HaveSuffix("..."), line+" should have been truncated")
}
}
})
It("podman search limit flag", func() {
search := podmanTest.PodmanExitCleanly("search", "--tls-verify=false", registryAddress+"/alpine")
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 10))
search = podmanTest.PodmanExitCleanly("search", "--limit", "3", "--tls-verify=false", registryAddress+"/alpine")
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToStringArray()).To(HaveLen(4))
search = podmanTest.PodmanExitCleanly("search", "--limit", "10", "--tls-verify=false", registryAddress+"/alpine")
Expect(search.OutputToStringArray()).To(HaveLen(11))
})
It("podman search with filter stars", func() {
search := podmanTest.PodmanExitCleanly("search", "--filter", "stars=10", "--format", "{{.Stars}}", "--tls-verify=false", registryAddress+"/alpine")
output := search.OutputToStringArray()
for i := range output {
Expect(strconv.Atoi(output[i])).To(BeNumerically(">=", 10))
}
})
It("podman search with filter is-official", func() {
search := podmanTest.PodmanExitCleanly("search", "--filter", "is-official", "--format", "{{.Official}}", "--tls-verify=false", registryAddress+"/alpine")
output := search.OutputToStringArray()
for i := range output {
Expect(output[i]).To(Equal("[OK]"))
}
})
It("podman search with filter is-automated", func() {
search := podmanTest.PodmanExitCleanly("search", "--filter", "is-automated=false", "--format", "{{.Automated}}", "--tls-verify=false", registryAddress+"/alpine")
output := search.OutputToStringArray()
for i := range output {
Expect(output[i]).To(Equal(""))
}
})
It("podman search format list tags with custom", func() {
search := podmanTest.PodmanExitCleanly("search", "--list-tags", "--format", "{{.Name}}", "--limit", "1", "--tls-verify=false", registryAddress+"/libpod/alpine")
Expect(search.OutputToString()).To(Equal(registryAddress + "/libpod/alpine"))
})
It("podman search with wildcards", func() {
search := podmanTest.PodmanExitCleanly("search", "--tls-verify=false", registryAddress+"/*alpine*")
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 1))
Expect(search.OutputToString()).To(ContainSubstring("alpine"))
})
It("podman search repository tags", func() {
search := podmanTest.PodmanExitCleanly("search", "--list-tags", "--limit", "30", "--tls-verify=false", registryAddress+"/podman/stable")
Expect(search.OutputToStringArray()).To(HaveLen(31))
search = podmanTest.PodmanExitCleanly("search", "--list-tags", "--tls-verify=false", registryAddress+"/podman/stable")
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 2))
search = podmanTest.Podman([]string{"search", "--filter=is-official", "--list-tags", "--tls-verify=false", registryAddress + "/podman/stable"})
search.WaitWithDefaultTimeout()
Expect(search).To(ExitWithError(125, "filters are not applicable to list tags result"))
// With trailing slash
search = podmanTest.Podman([]string{"search", "--list-tags", "--tls-verify=false", registryAddress + "/podman/"})
search.WaitWithDefaultTimeout()
Expect(search).To(ExitWithError(125, `reference "podman/" must be a docker reference`))
Expect(search.OutputToStringArray()).To(BeEmpty())
// No trailing slash
search = podmanTest.Podman([]string{"search", "--list-tags", "--tls-verify=false", registryAddress + "/podman"})
search.WaitWithDefaultTimeout()
Expect(search).To(ExitWithError(125, "getting repository tags: fetching tags list: StatusCode: 404"))
Expect(search.OutputToStringArray()).To(BeEmpty())
})
It("podman search with limit over 100", func() {
search := podmanTest.PodmanExitCleanly("search", "--limit", "100", "--tls-verify=false", registryAddress+"/podman")
Expect(len(search.OutputToStringArray())).To(BeNumerically("<=", 101))
})
// podman search v2 registry with empty query
searchEmpty := podmanTest.Podman([]string{"search", fmt.Sprintf("%s/", ep.Address()), "--tls-verify=false"})
searchEmpty.WaitWithDefaultTimeout()
Expect(searchEmpty).Should(ExitCleanly())
Expect(searchEmpty.OutputToStringArray()).ToNot(BeEmpty())
Expect(search.OutputToString()).To(ContainSubstring("my-alpine"))
})
It("podman search attempts HTTP if registry is in registries.insecure and force secure is false", func() {
if podmanTest.Host.Arch == "ppc64le" {
Skip("No registry image for ppc64le")
Context("podman search with container-based registries", func() {
var ep endpoint
var image string
var registryName string
var port int64
setupRegistryConfig := func(ep endpoint, registryName string, template *template.Template) {
var buffer bytes.Buffer
err := template.Execute(&buffer, ep)
Expect(err).ToNot(HaveOccurred())
podmanTest.setRegistriesConfigEnv(buffer.Bytes())
err = os.WriteFile(fmt.Sprintf("%s/%s.conf", tempdir, registryName), buffer.Bytes(), 0o644)
Expect(err).ToNot(HaveOccurred())
}
port := GetPort()
ep := endpoint{Port: strconv.Itoa(port), Host: "localhost"}
registry := podmanTest.Podman([]string{"run", "-d", "-p", fmt.Sprintf("%d:5000", port),
"--name", "registry4", REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"})
registry.WaitWithDefaultTimeout()
Expect(registry).Should(ExitCleanly())
BeforeEach(func() {
registryName = fmt.Sprintf("registry%d", GinkgoRandomSeed())
ep = mockFakeRegistryServerAsContainer(registryName)
image = pushAlpineImageIntoMockRegistry(ep)
if !WaitContainerReady(podmanTest, "registry4", "listening on", 20, 1) {
Fail("unable to start registry on port %s", port)
}
port, err = strconv.ParseInt(ep.Port, 10, 64)
Expect(err).ToNot(HaveOccurred())
})
err = podmanTest.RestoreArtifact(ALPINE)
Expect(err).ToNot(HaveOccurred())
image := fmt.Sprintf("%s/my-alpine", ep.Address())
push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, image})
push.WaitWithDefaultTimeout()
Expect(push).Should(ExitCleanly())
AfterEach(func() {
resetRegistriesConfigEnv()
podmanTest.PodmanExitCleanly("rm", "-f", registryName)
})
// registries.conf set up
var buffer bytes.Buffer
err = registryFileTmpl.Execute(&buffer, ep)
Expect(err).ToNot(HaveOccurred())
podmanTest.setRegistriesConfigEnv(buffer.Bytes())
err = os.WriteFile(fmt.Sprintf("%s/registry4.conf", tempdir), buffer.Bytes(), 0o644)
Expect(err).ToNot(HaveOccurred())
if IsRemote() {
podmanTest.RestartRemoteService()
defer podmanTest.RestartRemoteService()
}
It("podman search attempts HTTP if tls-verify flag is set false", func() {
// if this test succeeded, there will be no output (there is no entry named fake/image:andtag in an empty registry)
// and the exit code will be 0
search := podmanTest.PodmanExitCleanly("search", fmt.Sprintf("%s/fake/image:andtag", ep.Address()), "--tls-verify=false")
Expect(search.OutputToString()).Should(BeEmpty())
})
search := podmanTest.Podman([]string{"search", image})
search.WaitWithDefaultTimeout()
It("podman search in local registry", func() {
search := podmanTest.PodmanExitCleanly("search", image, "--tls-verify=false")
Expect(search.OutputToString()).ShouldNot(BeEmpty())
Expect(search).Should(ExitCleanly())
Expect(search.OutputToString()).To(ContainSubstring("my-alpine"))
// podman search v2 registry with empty query
searchEmpty := podmanTest.PodmanExitCleanly("search", fmt.Sprintf("%s/", ep.Address()), "--tls-verify=false")
Expect(searchEmpty.OutputToStringArray()).ToNot(BeEmpty())
Expect(search.OutputToString()).To(ContainSubstring("my-alpine"))
})
// cleanup
resetRegistriesConfigEnv()
})
It("podman search attempts HTTP if registry is in registries.insecure and force secure is false", func() {
// registries.conf set up
setupRegistryConfig(ep, registryName, registryFileTmpl)
if IsRemote() {
podmanTest.RestartRemoteService()
defer podmanTest.RestartRemoteService()
}
It("podman search doesn't attempt HTTP if force secure is true", func() {
if podmanTest.Host.Arch == "ppc64le" {
Skip("No registry image for ppc64le")
}
port := GetPort()
ep := endpoint{Port: strconv.Itoa(port), Host: "localhost"}
registry := podmanTest.Podman([]string{"run", "-d", "-p", fmt.Sprintf("%d:5000", port),
"--name", "registry5", REGISTRY_IMAGE})
registry.WaitWithDefaultTimeout()
Expect(registry).Should(ExitCleanly())
search := podmanTest.PodmanExitCleanly("search", image)
Expect(search.OutputToString()).To(ContainSubstring("my-alpine"))
})
if !WaitContainerReady(podmanTest, "registry5", "listening on", 20, 1) {
Fail("Cannot start docker registry on port %s", port)
}
It("podman search doesn't attempt HTTP if force secure is true", func() {
setupRegistryConfig(ep, registryName, registryFileTmpl)
if IsRemote() {
podmanTest.RestartRemoteService()
defer podmanTest.RestartRemoteService()
}
err = podmanTest.RestoreArtifact(ALPINE)
Expect(err).ToNot(HaveOccurred())
image := fmt.Sprintf("%s/my-alpine", ep.Address())
push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, image})
push.WaitWithDefaultTimeout()
Expect(push).Should(ExitCleanly())
search := podmanTest.Podman([]string{"search", image, "--tls-verify=true"})
search.WaitWithDefaultTimeout()
var buffer bytes.Buffer
err = registryFileTmpl.Execute(&buffer, ep)
Expect(err).ToNot(HaveOccurred())
podmanTest.setRegistriesConfigEnv(buffer.Bytes())
err = os.WriteFile(fmt.Sprintf("%s/registry5.conf", tempdir), buffer.Bytes(), 0o644)
Expect(err).ToNot(HaveOccurred())
Expect(search).Should(ExitWithError(125, fmt.Sprintf(`couldn't search registry "localhost:%d": pinging container registry localhost:%d: Get "https://localhost:%d/v2/": http: server gave HTTP response to HTTPS client`, port, port, port)))
Expect(search.OutputToString()).Should(BeEmpty())
})
search := podmanTest.Podman([]string{"search", image, "--tls-verify=true"})
search.WaitWithDefaultTimeout()
It("podman search doesn't attempt HTTP if registry is not listed as insecure", func() {
setupRegistryConfig(ep, registryName, registryFileBadTmpl)
if IsRemote() {
podmanTest.RestartRemoteService()
defer podmanTest.RestartRemoteService()
}
Expect(search).Should(ExitWithError(125, fmt.Sprintf(`couldn't search registry "localhost:%d": pinging container registry localhost:%d: Get "https://localhost:%d/v2/": http: server gave HTTP response to HTTPS client`, port, port, port)))
Expect(search.OutputToString()).Should(BeEmpty())
search := podmanTest.Podman([]string{"search", image})
search.WaitWithDefaultTimeout()
// cleanup
resetRegistriesConfigEnv()
})
It("podman search doesn't attempt HTTP if registry is not listed as insecure", func() {
if podmanTest.Host.Arch == "ppc64le" {
Skip("No registry image for ppc64le")
}
port := GetPort()
ep := endpoint{Port: strconv.Itoa(port), Host: "localhost"}
registry := podmanTest.Podman([]string{"run", "-d", "-p", fmt.Sprintf("%d:5000", port),
"--name", "registry6", REGISTRY_IMAGE})
registry.WaitWithDefaultTimeout()
Expect(registry).Should(ExitCleanly())
if !WaitContainerReady(podmanTest, "registry6", "listening on", 20, 1) {
Fail("Cannot start docker registry on port %s", port)
}
err = podmanTest.RestoreArtifact(ALPINE)
Expect(err).ToNot(HaveOccurred())
image := fmt.Sprintf("%s/my-alpine", ep.Address())
push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, image})
push.WaitWithDefaultTimeout()
Expect(push).Should(ExitCleanly())
var buffer bytes.Buffer
err = registryFileBadTmpl.Execute(&buffer, ep)
Expect(err).ToNot(HaveOccurred())
podmanTest.setRegistriesConfigEnv(buffer.Bytes())
err = os.WriteFile(fmt.Sprintf("%s/registry6.conf", tempdir), buffer.Bytes(), 0o644)
Expect(err).ToNot(HaveOccurred())
if IsRemote() {
podmanTest.RestartRemoteService()
defer podmanTest.RestartRemoteService()
}
search := podmanTest.Podman([]string{"search", image})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitWithError(125, fmt.Sprintf(`couldn't search registry "localhost:%d": pinging container registry localhost:%d: Get "https://localhost:%d/v2/": http: server gave HTTP response to HTTPS client`, port, port, port)))
Expect(search.OutputToString()).Should(BeEmpty())
// cleanup
resetRegistriesConfigEnv()
Expect(search).Should(ExitWithError(125, fmt.Sprintf(`couldn't search registry "localhost:%d": pinging container registry localhost:%d: Get "https://localhost:%d/v2/": http: server gave HTTP response to HTTPS client`, port, port, port)))
Expect(search.OutputToString()).Should(BeEmpty())
})
})
// search should fail with nonexistent authfile
@@ -372,47 +320,4 @@ registries = []`
search.WaitWithDefaultTimeout()
Expect(search).To(ExitWithError(125, "credential file is not accessible: faccessat /tmp/nonexistent: no such file or directory"))
})
// Registry is unreliable (#18484), this is another super-common flake
It("podman search with wildcards", FlakeAttempts(3), func() {
search := podmanTest.Podman([]string{"search", "registry.access.redhat.com/*openshift*"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 1))
})
It("podman search repository tags", func() {
search := podmanTest.Podman([]string{"search", "--list-tags", "--limit", "30", "quay.io/podman/stable"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(search.OutputToStringArray()).To(HaveLen(31))
search = podmanTest.Podman([]string{"search", "--list-tags", "quay.io/podman/stable"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(len(search.OutputToStringArray())).To(BeNumerically(">", 2))
search = podmanTest.Podman([]string{"search", "--filter=is-official", "--list-tags", "quay.io/podman/stable"})
search.WaitWithDefaultTimeout()
Expect(search).To(ExitWithError(125, "filters are not applicable to list tags result"))
// With trailing slash
search = podmanTest.Podman([]string{"search", "--list-tags", "quay.io/podman/"})
search.WaitWithDefaultTimeout()
Expect(search).To(ExitWithError(125, `reference "podman/" must be a docker reference`))
Expect(search.OutputToStringArray()).To(BeEmpty())
// No trailing slash
search = podmanTest.Podman([]string{"search", "--list-tags", "quay.io/podman"})
search.WaitWithDefaultTimeout()
Expect(search).To(ExitWithError(125, "getting repository tags: fetching tags list: StatusCode: 404"))
Expect(search.OutputToStringArray()).To(BeEmpty())
})
It("podman search with limit over 100", func() {
search := podmanTest.Podman([]string{"search", "--limit", "100", "quay.io/podman"})
search.WaitWithDefaultTimeout()
Expect(search).Should(ExitCleanly())
Expect(len(search.OutputToStringArray())).To(BeNumerically("<=", 101))
})
})