github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/libpod/image/search.go (about) 1 package image 2 3 import ( 4 "context" 5 "fmt" 6 "strconv" 7 "strings" 8 "sync" 9 10 "github.com/containers/image/v5/docker" 11 "github.com/containers/image/v5/transports/alltransports" 12 "github.com/containers/image/v5/types" 13 sysreg "github.com/containers/podman/v2/pkg/registries" 14 "github.com/pkg/errors" 15 "github.com/sirupsen/logrus" 16 "golang.org/x/sync/semaphore" 17 ) 18 19 const ( 20 descriptionTruncLength = 44 21 maxQueries = 25 22 maxParallelSearches = int64(6) 23 ) 24 25 // SearchResult is holding image-search related data. 26 type SearchResult struct { 27 // Index is the image index (e.g., "docker.io" or "quay.io") 28 Index string 29 // Name is the canoncical name of the image (e.g., "docker.io/library/alpine"). 30 Name string 31 // Description of the image. 32 Description string 33 // Stars is the number of stars of the image. 34 Stars int 35 // Official indicates if it's an official image. 36 Official string 37 // Automated indicates if the image was created by an automated build. 38 Automated string 39 // Tag is the image tag 40 Tag string 41 } 42 43 // SearchOptions are used to control the behaviour of SearchImages. 44 type SearchOptions struct { 45 // Filter allows to filter the results. 46 Filter SearchFilter 47 // Limit limits the number of queries per index (default: 25). Must be 48 // greater than 0 to overwrite the default value. 49 Limit int 50 // NoTrunc avoids the output to be truncated. 51 NoTrunc bool 52 // Authfile is the path to the authentication file. 53 Authfile string 54 // InsecureSkipTLSVerify allows to skip TLS verification. 55 InsecureSkipTLSVerify types.OptionalBool 56 // ListTags returns the search result with available tags 57 ListTags bool 58 } 59 60 // SearchFilter allows filtering the results of SearchImages. 61 type SearchFilter struct { 62 // Stars describes the minimal amount of starts of an image. 63 Stars int 64 // IsAutomated decides if only images from automated builds are displayed. 65 IsAutomated types.OptionalBool 66 // IsOfficial decides if only official images are displayed. 67 IsOfficial types.OptionalBool 68 } 69 70 // SearchImages searches images based on term and the specified SearchOptions 71 // in all registries. 72 func SearchImages(term string, options SearchOptions) ([]SearchResult, error) { 73 registry := "" 74 75 // Try to extract a registry from the specified search term. We 76 // consider everything before the first slash to be the registry. Note 77 // that we cannot use the reference parser from the containers/image 78 // library as the search term may container arbitrary input such as 79 // wildcards. See bugzilla.redhat.com/show_bug.cgi?id=1846629. 80 if spl := strings.SplitN(term, "/", 2); len(spl) > 1 { 81 registry = spl[0] 82 term = spl[1] 83 } 84 85 registries, err := getRegistries(registry) 86 if err != nil { 87 return nil, err 88 } 89 90 // searchOutputData is used as a return value for searching in parallel. 91 type searchOutputData struct { 92 data []SearchResult 93 err error 94 } 95 96 // Let's follow Firefox by limiting parallel downloads to 6. 97 sem := semaphore.NewWeighted(maxParallelSearches) 98 wg := sync.WaitGroup{} 99 wg.Add(len(registries)) 100 data := make([]searchOutputData, len(registries)) 101 102 searchImageInRegistryHelper := func(index int, registry string) { 103 defer sem.Release(1) 104 defer wg.Done() 105 searchOutput := searchImageInRegistry(term, registry, options) 106 data[index] = searchOutputData{data: searchOutput} 107 } 108 109 ctx := context.Background() 110 for i := range registries { 111 if err := sem.Acquire(ctx, 1); err != nil { 112 return nil, err 113 } 114 go searchImageInRegistryHelper(i, registries[i]) 115 } 116 117 wg.Wait() 118 results := []SearchResult{} 119 for _, d := range data { 120 if d.err != nil { 121 return nil, d.err 122 } 123 results = append(results, d.data...) 124 } 125 return results, nil 126 } 127 128 // getRegistries returns the list of registries to search, depending on an optional registry specification 129 func getRegistries(registry string) ([]string, error) { 130 var registries []string 131 if registry != "" { 132 registries = append(registries, registry) 133 } else { 134 var err error 135 registries, err = sysreg.GetRegistries() 136 if err != nil { 137 return nil, errors.Wrapf(err, "error getting registries to search") 138 } 139 } 140 return registries, nil 141 } 142 143 func searchImageInRegistry(term string, registry string, options SearchOptions) []SearchResult { 144 // Max number of queries by default is 25 145 limit := maxQueries 146 if options.Limit > 0 { 147 limit = options.Limit 148 } 149 150 sc := GetSystemContext("", options.Authfile, false) 151 sc.DockerInsecureSkipTLSVerify = options.InsecureSkipTLSVerify 152 // FIXME: Set this more globally. Probably no reason not to have it in 153 // every types.SystemContext, and to compute the value just once in one 154 // place. 155 sc.SystemRegistriesConfPath = sysreg.SystemRegistriesConfPath() 156 if options.ListTags { 157 results, err := searchRepositoryTags(registry, term, sc, options) 158 if err != nil { 159 logrus.Errorf("error listing registry tags %q: %v", registry, err) 160 return []SearchResult{} 161 } 162 return results 163 } 164 165 results, err := docker.SearchRegistry(context.TODO(), sc, registry, term, limit) 166 if err != nil { 167 logrus.Errorf("error searching registry %q: %v", registry, err) 168 return []SearchResult{} 169 } 170 index := registry 171 arr := strings.Split(registry, ".") 172 if len(arr) > 2 { 173 index = strings.Join(arr[len(arr)-2:], ".") 174 } 175 176 // limit is the number of results to output 177 // if the total number of results is less than the limit, output all 178 // if the limit has been set by the user, output those number of queries 179 limit = maxQueries 180 if len(results) < limit { 181 limit = len(results) 182 } 183 if options.Limit != 0 { 184 limit = len(results) 185 if options.Limit < len(results) { 186 limit = options.Limit 187 } 188 } 189 190 paramsArr := []SearchResult{} 191 for i := 0; i < limit; i++ { 192 // Check whether query matches filters 193 if !(options.Filter.matchesAutomatedFilter(results[i]) && options.Filter.matchesOfficialFilter(results[i]) && options.Filter.matchesStarFilter(results[i])) { 194 continue 195 } 196 official := "" 197 if results[i].IsOfficial { 198 official = "[OK]" 199 } 200 automated := "" 201 if results[i].IsAutomated { 202 automated = "[OK]" 203 } 204 description := strings.Replace(results[i].Description, "\n", " ", -1) 205 if len(description) > 44 && !options.NoTrunc { 206 description = description[:descriptionTruncLength] + "..." 207 } 208 name := registry + "/" + results[i].Name 209 if index == "docker.io" && !strings.Contains(results[i].Name, "/") { 210 name = index + "/library/" + results[i].Name 211 } 212 params := SearchResult{ 213 Index: index, 214 Name: name, 215 Description: description, 216 Official: official, 217 Automated: automated, 218 Stars: results[i].StarCount, 219 } 220 paramsArr = append(paramsArr, params) 221 } 222 return paramsArr 223 } 224 225 func searchRepositoryTags(registry, term string, sc *types.SystemContext, options SearchOptions) ([]SearchResult, error) { 226 dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) 227 imageRef, err := alltransports.ParseImageName(fmt.Sprintf("%s/%s", registry, term)) 228 if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { 229 return nil, errors.Errorf("reference %q must be a docker reference", term) 230 } else if err != nil { 231 imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, fmt.Sprintf("%s/%s", registry, term))) 232 if err != nil { 233 return nil, errors.Errorf("reference %q must be a docker reference", term) 234 } 235 } 236 tags, err := docker.GetRepositoryTags(context.TODO(), sc, imageRef) 237 if err != nil { 238 return nil, errors.Errorf("error getting repository tags: %v", err) 239 } 240 limit := maxQueries 241 if len(tags) < limit { 242 limit = len(tags) 243 } 244 if options.Limit != 0 { 245 limit = len(tags) 246 if options.Limit < limit { 247 limit = options.Limit 248 } 249 } 250 paramsArr := []SearchResult{} 251 for i := 0; i < limit; i++ { 252 params := SearchResult{ 253 Name: imageRef.DockerReference().Name(), 254 Tag: tags[i], 255 } 256 paramsArr = append(paramsArr, params) 257 } 258 return paramsArr, nil 259 } 260 261 // ParseSearchFilter turns the filter into a SearchFilter that can be used for 262 // searching images. 263 func ParseSearchFilter(filter []string) (*SearchFilter, error) { 264 sFilter := new(SearchFilter) 265 for _, f := range filter { 266 arr := strings.SplitN(f, "=", 2) 267 switch arr[0] { 268 case "stars": 269 if len(arr) < 2 { 270 return nil, errors.Errorf("invalid `stars` filter %q, should be stars=<value>", filter) 271 } 272 stars, err := strconv.Atoi(arr[1]) 273 if err != nil { 274 return nil, errors.Wrapf(err, "incorrect value type for stars filter") 275 } 276 sFilter.Stars = stars 277 case "is-automated": 278 if len(arr) == 2 && arr[1] == "false" { 279 sFilter.IsAutomated = types.OptionalBoolFalse 280 } else { 281 sFilter.IsAutomated = types.OptionalBoolTrue 282 } 283 case "is-official": 284 if len(arr) == 2 && arr[1] == "false" { 285 sFilter.IsOfficial = types.OptionalBoolFalse 286 } else { 287 sFilter.IsOfficial = types.OptionalBoolTrue 288 } 289 default: 290 return nil, errors.Errorf("invalid filter type %q", f) 291 } 292 } 293 return sFilter, nil 294 } 295 296 func (f *SearchFilter) matchesStarFilter(result docker.SearchResult) bool { 297 return result.StarCount >= f.Stars 298 } 299 300 func (f *SearchFilter) matchesAutomatedFilter(result docker.SearchResult) bool { 301 if f.IsAutomated != types.OptionalBoolUndefined { 302 return result.IsAutomated == (f.IsAutomated == types.OptionalBoolTrue) 303 } 304 return true 305 } 306 307 func (f *SearchFilter) matchesOfficialFilter(result docker.SearchResult) bool { 308 if f.IsOfficial != types.OptionalBoolUndefined { 309 return result.IsOfficial == (f.IsOfficial == types.OptionalBoolTrue) 310 } 311 return true 312 }