zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/cli/client/service.go (about) 1 //go:build search 2 // +build search 3 4 package client 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "net/url" 12 "strconv" 13 "strings" 14 "sync" 15 16 "github.com/dustin/go-humanize" 17 jsoniter "github.com/json-iterator/go" 18 "github.com/olekukonko/tablewriter" 19 godigest "github.com/opencontainers/go-digest" 20 ispec "github.com/opencontainers/image-spec/specs-go/v1" 21 "gopkg.in/yaml.v2" 22 23 zerr "zotregistry.io/zot/errors" 24 "zotregistry.io/zot/pkg/api/constants" 25 "zotregistry.io/zot/pkg/common" 26 ) 27 28 const ( 29 jsonFormat = "json" 30 yamlFormat = "yaml" 31 ymlFormat = "yml" 32 ) 33 34 type SearchService interface { //nolint:interfacebloat 35 getImagesGQL(ctx context.Context, config SearchConfig, username, password string, 36 imageName string) (*common.ImageListResponse, error) 37 getImagesForDigestGQL(ctx context.Context, config SearchConfig, username, password string, 38 digest string) (*common.ImagesForDigest, error) 39 getCveByImageGQL(ctx context.Context, config SearchConfig, username, password, 40 imageName string, searchedCVE string) (*cveResult, error) 41 getTagsForCVEGQL(ctx context.Context, config SearchConfig, username, password, repo, 42 cveID string) (*common.ImagesForCve, error) 43 getFixedTagsForCVEGQL(ctx context.Context, config SearchConfig, username, password, imageName, 44 cveID string) (*common.ImageListWithCVEFixedResponse, error) 45 getDerivedImageListGQL(ctx context.Context, config SearchConfig, username, password string, 46 derivedImage string) (*common.DerivedImageListResponse, error) 47 getBaseImageListGQL(ctx context.Context, config SearchConfig, username, password string, 48 baseImage string) (*common.BaseImageListResponse, error) 49 getReferrersGQL(ctx context.Context, config SearchConfig, username, password string, 50 repo, digest string) (*common.ReferrersResp, error) 51 globalSearchGQL(ctx context.Context, config SearchConfig, username, password string, 52 query string) (*common.GlobalSearch, error) 53 54 getAllImages(ctx context.Context, config SearchConfig, username, password string, 55 channel chan stringResult, wtgrp *sync.WaitGroup) 56 getImagesByDigest(ctx context.Context, config SearchConfig, username, password, digest string, 57 channel chan stringResult, wtgrp *sync.WaitGroup) 58 getRepos(ctx context.Context, config SearchConfig, username, password string, 59 channel chan stringResult, wtgrp *sync.WaitGroup) 60 getImageByName(ctx context.Context, config SearchConfig, username, password, imageName string, 61 channel chan stringResult, wtgrp *sync.WaitGroup) 62 getReferrers(ctx context.Context, config SearchConfig, username, password string, repo, digest string, 63 ) (referrersResult, error) 64 } 65 66 type SearchConfig struct { 67 SearchService SearchService 68 ServURL string 69 User string 70 OutputFormat string 71 SortBy string 72 VerifyTLS bool 73 FixedFlag bool 74 Verbose bool 75 Debug bool 76 ResultWriter io.Writer 77 Spinner spinnerState 78 } 79 80 type searchService struct{} 81 82 func NewSearchService() SearchService { 83 return searchService{} 84 } 85 86 func (service searchService) getDerivedImageListGQL(ctx context.Context, config SearchConfig, username, password string, 87 derivedImage string, 88 ) (*common.DerivedImageListResponse, error) { 89 query := fmt.Sprintf(` 90 { 91 DerivedImageList(image:"%s", requestedPage: {sortBy: %s}){ 92 Results{ 93 RepoName Tag 94 Digest 95 MediaType 96 Manifests { 97 Digest 98 ConfigDigest 99 Size 100 Platform {Os Arch} 101 IsSigned 102 Layers {Size Digest} 103 LastUpdated 104 } 105 LastUpdated 106 Size 107 IsSigned 108 } 109 } 110 }`, derivedImage, Flag2SortCriteria(config.SortBy)) 111 112 result := &common.DerivedImageListResponse{} 113 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 114 115 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 116 return nil, errResult 117 } 118 119 return result, nil 120 } 121 122 func (service searchService) getReferrersGQL(ctx context.Context, config SearchConfig, username, password string, 123 repo, digest string, 124 ) (*common.ReferrersResp, error) { 125 query := fmt.Sprintf(` 126 { 127 Referrers( repo: "%s", digest: "%s" ){ 128 ArtifactType, 129 Digest, 130 MediaType, 131 Size, 132 Annotations{ 133 Key 134 Value 135 } 136 } 137 }`, repo, digest) 138 139 result := &common.ReferrersResp{} 140 141 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 142 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 143 return nil, errResult 144 } 145 146 return result, nil 147 } 148 149 func (service searchService) globalSearchGQL(ctx context.Context, config SearchConfig, username, password string, 150 query string, 151 ) (*common.GlobalSearch, error) { 152 GQLQuery := fmt.Sprintf(` 153 { 154 GlobalSearch(query:"%s", requestedPage: {sortBy: %s}){ 155 Images { 156 RepoName 157 Tag 158 MediaType 159 Digest 160 Size 161 IsSigned 162 LastUpdated 163 Manifests { 164 Digest 165 ConfigDigest 166 Platform {Os Arch} 167 Size 168 IsSigned 169 Layers {Size Digest} 170 LastUpdated 171 } 172 } 173 Repos { 174 Name 175 Platforms { Os Arch } 176 LastUpdated 177 Size 178 DownloadCount 179 StarCount 180 } 181 } 182 }`, query, Flag2SortCriteria(config.SortBy)) 183 184 result := &common.GlobalSearchResultResp{} 185 186 err := service.makeGraphQLQuery(ctx, config, username, password, GQLQuery, result) 187 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 188 return nil, errResult 189 } 190 191 return &result.GlobalSearch, nil 192 } 193 194 func (service searchService) getBaseImageListGQL(ctx context.Context, config SearchConfig, username, password string, 195 baseImage string, 196 ) (*common.BaseImageListResponse, error) { 197 query := fmt.Sprintf(` 198 { 199 BaseImageList(image:"%s", requestedPage: {sortBy: %s}){ 200 Results{ 201 RepoName Tag 202 Digest 203 MediaType 204 Manifests { 205 Digest 206 ConfigDigest 207 Size 208 Platform {Os Arch} 209 IsSigned 210 Layers {Size Digest} 211 LastUpdated 212 } 213 LastUpdated 214 Size 215 IsSigned 216 } 217 } 218 }`, baseImage, Flag2SortCriteria(config.SortBy)) 219 220 result := &common.BaseImageListResponse{} 221 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 222 223 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 224 return nil, errResult 225 } 226 227 return result, nil 228 } 229 230 func (service searchService) getImagesGQL(ctx context.Context, config SearchConfig, username, password string, 231 imageName string, 232 ) (*common.ImageListResponse, error) { 233 query := fmt.Sprintf(` 234 { 235 ImageList(repo: "%s", requestedPage: {sortBy: %s}) { 236 Results { 237 RepoName Tag 238 Digest 239 MediaType 240 Manifests { 241 Digest 242 ConfigDigest 243 Size 244 Platform {Os Arch} 245 IsSigned 246 Layers {Size Digest} 247 LastUpdated 248 } 249 LastUpdated 250 Size 251 IsSigned 252 } 253 } 254 }`, imageName, Flag2SortCriteria(config.SortBy)) 255 result := &common.ImageListResponse{} 256 257 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 258 259 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 260 return nil, errResult 261 } 262 263 return result, nil 264 } 265 266 func (service searchService) getImagesForDigestGQL(ctx context.Context, config SearchConfig, username, password string, 267 digest string, 268 ) (*common.ImagesForDigest, error) { 269 query := fmt.Sprintf(` 270 { 271 ImageListForDigest(id: "%s", requestedPage: {sortBy: %s}) { 272 Results { 273 RepoName Tag 274 Digest 275 MediaType 276 Manifests { 277 Digest 278 ConfigDigest 279 Size 280 Platform {Os Arch} 281 IsSigned 282 Layers {Size Digest} 283 LastUpdated 284 } 285 LastUpdated 286 Size 287 IsSigned 288 } 289 } 290 }`, digest, Flag2SortCriteria(config.SortBy)) 291 result := &common.ImagesForDigest{} 292 293 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 294 295 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 296 return nil, errResult 297 } 298 299 return result, nil 300 } 301 302 func (service searchService) getCveByImageGQL(ctx context.Context, config SearchConfig, username, password, 303 imageName, searchedCVE string, 304 ) (*cveResult, error) { 305 query := fmt.Sprintf(` 306 { 307 CVEListForImage (image:"%s", searchedCVE:"%s", requestedPage: {sortBy: %s}) { 308 Tag CVEList { 309 Id Title Severity Description 310 PackageList {Name InstalledVersion FixedVersion} 311 } 312 } 313 }`, imageName, searchedCVE, Flag2SortCriteria(config.SortBy)) 314 result := &cveResult{} 315 316 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 317 318 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 319 return nil, errResult 320 } 321 322 return result, nil 323 } 324 325 func (service searchService) getTagsForCVEGQL(ctx context.Context, config SearchConfig, 326 username, password, repo, cveID string, 327 ) (*common.ImagesForCve, error) { 328 query := fmt.Sprintf(` 329 { 330 ImageListForCVE(id: "%s", requestedPage: {sortBy: %s}) { 331 Results { 332 RepoName Tag 333 Digest 334 MediaType 335 Manifests { 336 Digest 337 ConfigDigest 338 Size 339 Platform {Os Arch} 340 IsSigned 341 Layers {Size Digest} 342 LastUpdated 343 } 344 LastUpdated 345 Size 346 IsSigned 347 } 348 } 349 }`, 350 cveID, Flag2SortCriteria(config.SortBy)) 351 result := &common.ImagesForCve{} 352 353 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 354 355 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 356 return nil, errResult 357 } 358 359 if repo == "" { 360 return result, nil 361 } 362 363 filteredResults := &common.ImagesForCve{} 364 365 for _, image := range result.Results { 366 if image.RepoName == repo { 367 filteredResults.Results = append(filteredResults.Results, image) 368 } 369 } 370 371 return filteredResults, nil 372 } 373 374 func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config SearchConfig, 375 username, password, imageName, cveID string, 376 ) (*common.ImageListWithCVEFixedResponse, error) { 377 query := fmt.Sprintf(` 378 { 379 ImageListWithCVEFixed(id: "%s", image: "%s") { 380 Results { 381 RepoName Tag 382 Digest 383 MediaType 384 Manifests { 385 Digest 386 ConfigDigest 387 Size 388 Platform {Os Arch} 389 IsSigned 390 Layers {Size Digest} 391 LastUpdated 392 } 393 LastUpdated 394 Size 395 IsSigned 396 } 397 } 398 }`, 399 cveID, imageName) 400 401 result := &common.ImageListWithCVEFixedResponse{} 402 403 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 404 405 if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { 406 return nil, errResult 407 } 408 409 return result, nil 410 } 411 412 func (service searchService) getReferrers(ctx context.Context, config SearchConfig, username, password string, 413 repo, digest string, 414 ) (referrersResult, error) { 415 referrersEndpoint, err := combineServerAndEndpointURL(config.ServURL, 416 fmt.Sprintf("/v2/%s/referrers/%s", repo, digest)) 417 if err != nil { 418 if common.IsContextDone(ctx) { 419 return referrersResult{}, nil 420 } 421 422 return referrersResult{}, err 423 } 424 425 referrerResp := &ispec.Index{} 426 _, err = makeGETRequest(ctx, referrersEndpoint, username, password, config.VerifyTLS, 427 config.Debug, &referrerResp, config.ResultWriter) 428 429 if err != nil { 430 if common.IsContextDone(ctx) { 431 return referrersResult{}, nil 432 } 433 434 return referrersResult{}, err 435 } 436 437 referrersList := referrersResult{} 438 439 for _, referrer := range referrerResp.Manifests { 440 referrersList = append(referrersList, common.Referrer{ 441 ArtifactType: referrer.ArtifactType, 442 Digest: referrer.Digest.String(), 443 Size: int(referrer.Size), 444 }) 445 } 446 447 return referrersList, nil 448 } 449 450 func (service searchService) getImageByName(ctx context.Context, config SearchConfig, 451 username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup, 452 ) { 453 defer wtgrp.Done() 454 defer close(rch) 455 456 var localWg sync.WaitGroup 457 rlim := newSmoothRateLimiter(&localWg, rch) 458 459 localWg.Add(1) 460 461 go rlim.startRateLimiter(ctx) 462 localWg.Add(1) 463 464 go getImage(ctx, config, username, password, imageName, rch, &localWg, rlim) 465 466 localWg.Wait() 467 } 468 469 func (service searchService) getAllImages(ctx context.Context, config SearchConfig, username, password string, 470 rch chan stringResult, wtgrp *sync.WaitGroup, 471 ) { 472 defer wtgrp.Done() 473 defer close(rch) 474 475 catalog := &catalogResponse{} 476 477 catalogEndPoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("%s%s", 478 constants.RoutePrefix, constants.ExtCatalogPrefix)) 479 if err != nil { 480 if common.IsContextDone(ctx) { 481 return 482 } 483 rch <- stringResult{"", err} 484 485 return 486 } 487 488 _, err = makeGETRequest(ctx, catalogEndPoint, username, password, config.VerifyTLS, 489 config.Debug, catalog, config.ResultWriter) 490 if err != nil { 491 if common.IsContextDone(ctx) { 492 return 493 } 494 rch <- stringResult{"", err} 495 496 return 497 } 498 499 var localWg sync.WaitGroup 500 501 rlim := newSmoothRateLimiter(&localWg, rch) 502 503 localWg.Add(1) 504 505 go rlim.startRateLimiter(ctx) 506 507 for _, repo := range catalog.Repositories { 508 localWg.Add(1) 509 510 go getImage(ctx, config, username, password, repo, rch, &localWg, rlim) 511 } 512 513 localWg.Wait() 514 } 515 516 func getImage(ctx context.Context, config SearchConfig, username, password, imageName string, 517 rch chan stringResult, wtgrp *sync.WaitGroup, pool *requestsPool, 518 ) { 519 defer wtgrp.Done() 520 521 repo, imageTag := common.GetImageDirAndTag(imageName) 522 523 tagListEndpoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("/v2/%s/tags/list", repo)) 524 if err != nil { 525 if common.IsContextDone(ctx) { 526 return 527 } 528 rch <- stringResult{"", err} 529 530 return 531 } 532 533 tagList := &tagListResp{} 534 _, err = makeGETRequest(ctx, tagListEndpoint, username, password, config.VerifyTLS, 535 config.Debug, &tagList, config.ResultWriter) 536 537 if err != nil { 538 if common.IsContextDone(ctx) { 539 return 540 } 541 rch <- stringResult{"", err} 542 543 return 544 } 545 546 for _, tag := range tagList.Tags { 547 hasTagPrefix := strings.HasPrefix(tag, "sha256-") 548 hasTagSuffix := strings.HasSuffix(tag, ".sig") 549 550 // check if it's an image or a signature 551 // we don't want to show signatures in cli responses 552 if hasTagPrefix && hasTagSuffix { 553 continue 554 } 555 556 shouldMatchTag := imageTag != "" 557 matchesTag := tag == imageTag 558 559 // when the tag is empty we match everything 560 if shouldMatchTag && !matchesTag { 561 continue 562 } 563 564 wtgrp.Add(1) 565 566 go addManifestCallToPool(ctx, config, pool, username, password, repo, tag, rch, wtgrp) 567 } 568 } 569 570 func (service searchService) getImagesByDigest(ctx context.Context, config SearchConfig, username, 571 password string, digest string, rch chan stringResult, wtgrp *sync.WaitGroup, 572 ) { 573 defer wtgrp.Done() 574 defer close(rch) 575 576 query := fmt.Sprintf( 577 `{ 578 ImageListForDigest(id: "%s") { 579 Results { 580 RepoName Tag 581 Digest 582 MediaType 583 Manifests { 584 Digest 585 ConfigDigest 586 Size 587 Platform {Os Arch} 588 IsSigned 589 Layers {Size Digest} 590 LastUpdated 591 } 592 LastUpdated 593 Size 594 IsSigned 595 } 596 } 597 }`, 598 digest) 599 600 result := &common.ImagesForDigest{} 601 602 err := service.makeGraphQLQuery(ctx, config, username, password, query, result) 603 if err != nil { 604 if common.IsContextDone(ctx) { 605 return 606 } 607 rch <- stringResult{"", err} 608 609 return 610 } 611 612 if result.Errors != nil { 613 var errBuilder strings.Builder 614 615 for _, err := range result.Errors { 616 fmt.Fprintln(&errBuilder, err.Message) 617 } 618 619 if common.IsContextDone(ctx) { 620 return 621 } 622 rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 623 624 return 625 } 626 627 var localWg sync.WaitGroup 628 629 rlim := newSmoothRateLimiter(&localWg, rch) 630 localWg.Add(1) 631 632 go rlim.startRateLimiter(ctx) 633 634 for _, image := range result.Results { 635 localWg.Add(1) 636 637 go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg) 638 } 639 640 localWg.Wait() 641 } 642 643 // Query using GQL, the query string is passed as a parameter 644 // errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr. 645 func (service searchService) makeGraphQLQuery(ctx context.Context, 646 config SearchConfig, username, password, query string, 647 resultPtr interface{}, 648 ) error { 649 endPoint, err := combineServerAndEndpointURL(config.ServURL, constants.FullSearchPrefix) 650 if err != nil { 651 return err 652 } 653 654 err = makeGraphQLRequest(ctx, endPoint, query, username, password, config.VerifyTLS, 655 config.Debug, resultPtr, config.ResultWriter) 656 if err != nil { 657 return err 658 } 659 660 return nil 661 } 662 663 func checkResultGraphQLQuery(ctx context.Context, err error, resultErrors []common.ErrorGQL, 664 ) error { 665 if err != nil { 666 if common.IsContextDone(ctx) { 667 return nil //nolint:nilnil 668 } 669 670 return err 671 } 672 673 if resultErrors != nil { 674 var errBuilder strings.Builder 675 676 for _, error := range resultErrors { 677 fmt.Fprintln(&errBuilder, error.Message) 678 } 679 680 if common.IsContextDone(ctx) { 681 return nil 682 } 683 684 //nolint: goerr113 685 return errors.New(errBuilder.String()) 686 } 687 688 return nil 689 } 690 691 func addManifestCallToPool(ctx context.Context, config SearchConfig, pool *requestsPool, 692 username, password, imageName, tagName string, rch chan stringResult, wtgrp *sync.WaitGroup, 693 ) { 694 defer wtgrp.Done() 695 696 manifestEndpoint, err := combineServerAndEndpointURL(config.ServURL, 697 fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName)) 698 if err != nil { 699 if common.IsContextDone(ctx) { 700 return 701 } 702 rch <- stringResult{"", err} 703 } 704 705 job := httpJob{ 706 url: manifestEndpoint, 707 username: username, 708 imageName: imageName, 709 password: password, 710 tagName: tagName, 711 config: config, 712 } 713 714 wtgrp.Add(1) 715 pool.submitJob(&job) 716 } 717 718 type cveResult struct { 719 Errors []common.ErrorGQL `json:"errors"` 720 Data cveData `json:"data"` 721 } 722 723 type tagListResp struct { 724 Name string `json:"name"` 725 Tags []string `json:"tags"` 726 } 727 728 //nolint:tagliatelle // graphQL schema 729 type packageList struct { 730 Name string `json:"Name"` 731 InstalledVersion string `json:"InstalledVersion"` 732 FixedVersion string `json:"FixedVersion"` 733 } 734 735 //nolint:tagliatelle // graphQL schema 736 type cve struct { 737 ID string `json:"Id"` 738 Severity string `json:"Severity"` 739 Title string `json:"Title"` 740 Description string `json:"Description"` 741 PackageList []packageList `json:"PackageList"` 742 } 743 744 //nolint:tagliatelle // graphQL schema 745 type cveListForImage struct { 746 Tag string `json:"Tag"` 747 CVEList []cve `json:"CVEList"` 748 } 749 750 //nolint:tagliatelle // graphQL schema 751 type cveData struct { 752 CVEListForImage cveListForImage `json:"CVEListForImage"` 753 } 754 755 func (cve cveResult) string(format string) (string, error) { 756 switch strings.ToLower(format) { 757 case "", defaultOutputFormat: 758 return cve.stringPlainText() 759 case jsonFormat: 760 return cve.stringJSON() 761 case ymlFormat, yamlFormat: 762 return cve.stringYAML() 763 default: 764 return "", zerr.ErrInvalidOutputFormat 765 } 766 } 767 768 func (cve cveResult) stringPlainText() (string, error) { 769 var builder strings.Builder 770 771 table := getCVETableWriter(&builder) 772 773 for _, c := range cve.Data.CVEListForImage.CVEList { 774 id := ellipsize(c.ID, cveIDWidth, ellipsis) 775 title := ellipsize(c.Title, cveTitleWidth, ellipsis) 776 severity := ellipsize(c.Severity, cveSeverityWidth, ellipsis) 777 row := make([]string, 3) //nolint:gomnd 778 row[colCVEIDIndex] = id 779 row[colCVESeverityIndex] = severity 780 row[colCVETitleIndex] = title 781 782 table.Append(row) 783 } 784 785 table.Render() 786 787 return builder.String(), nil 788 } 789 790 func (cve cveResult) stringJSON() (string, error) { 791 // Output is in json lines format - do not indent, append new line after json 792 json := jsoniter.ConfigCompatibleWithStandardLibrary 793 794 body, err := json.Marshal(cve.Data.CVEListForImage) 795 if err != nil { 796 return "", err 797 } 798 799 return string(body) + "\n", nil 800 } 801 802 func (cve cveResult) stringYAML() (string, error) { 803 // Output will be a multidoc yaml - use triple-dash to indicate a new document 804 body, err := yaml.Marshal(&cve.Data.CVEListForImage) 805 if err != nil { 806 return "", err 807 } 808 809 return "---\n" + string(body), nil 810 } 811 812 type referrersResult []common.Referrer 813 814 func (ref referrersResult) string(format string, maxArtifactTypeLen int) (string, error) { 815 switch strings.ToLower(format) { 816 case "", defaultOutputFormat: 817 return ref.stringPlainText(maxArtifactTypeLen) 818 case jsonFormat: 819 return ref.stringJSON() 820 case ymlFormat, yamlFormat: 821 return ref.stringYAML() 822 default: 823 return "", zerr.ErrInvalidOutputFormat 824 } 825 } 826 827 func (ref referrersResult) stringPlainText(maxArtifactTypeLen int) (string, error) { 828 var builder strings.Builder 829 830 table := getImageTableWriter(&builder) 831 832 table.SetColMinWidth(refArtifactTypeIndex, maxArtifactTypeLen) 833 table.SetColMinWidth(refDigestIndex, digestWidth) 834 table.SetColMinWidth(refSizeIndex, sizeWidth) 835 836 for _, referrer := range ref { 837 artifactType := ellipsize(referrer.ArtifactType, maxArtifactTypeLen, ellipsis) 838 // digest := ellipsize(godigest.Digest(referrer.Digest).Encoded(), digestWidth, "") 839 size := ellipsize(humanize.Bytes(uint64(referrer.Size)), sizeWidth, ellipsis) 840 841 row := make([]string, refRowWidth) 842 row[refArtifactTypeIndex] = artifactType 843 row[refDigestIndex] = referrer.Digest 844 row[refSizeIndex] = size 845 846 table.Append(row) 847 } 848 849 table.Render() 850 851 return builder.String(), nil 852 } 853 854 func (ref referrersResult) stringJSON() (string, error) { 855 // Output is in json lines format - do not indent, append new line after json 856 json := jsoniter.ConfigCompatibleWithStandardLibrary 857 858 body, err := json.Marshal(ref) 859 if err != nil { 860 return "", err 861 } 862 863 return string(body) + "\n", nil 864 } 865 866 func (ref referrersResult) stringYAML() (string, error) { 867 // Output will be a multidoc yaml - use triple-dash to indicate a new document 868 body, err := yaml.Marshal(ref) 869 if err != nil { 870 return "", err 871 } 872 873 return "---\n" + string(body), nil 874 } 875 876 type repoStruct common.RepoSummary 877 878 func (repo repoStruct) string(format string, maxImgNameLen, maxTimeLen int, verbose bool) (string, error) { //nolint: lll 879 switch strings.ToLower(format) { 880 case "", defaultOutputFormat: 881 return repo.stringPlainText(maxImgNameLen, maxTimeLen, verbose) 882 case jsonFormat: 883 return repo.stringJSON() 884 case ymlFormat, yamlFormat: 885 return repo.stringYAML() 886 default: 887 return "", zerr.ErrInvalidOutputFormat 888 } 889 } 890 891 func (repo repoStruct) stringPlainText(repoMaxLen, maxTimeLen int, verbose bool) (string, error) { 892 var builder strings.Builder 893 894 table := getImageTableWriter(&builder) 895 896 table.SetColMinWidth(repoNameIndex, repoMaxLen) 897 table.SetColMinWidth(repoSizeIndex, sizeWidth) 898 table.SetColMinWidth(repoLastUpdatedIndex, maxTimeLen) 899 table.SetColMinWidth(repoDownloadsIndex, downloadsWidth) 900 table.SetColMinWidth(repoStarsIndex, signedWidth) 901 902 if verbose { 903 table.SetColMinWidth(repoPlatformsIndex, platformWidth) 904 } 905 906 repoSize, err := strconv.Atoi(repo.Size) 907 if err != nil { 908 return "", err 909 } 910 911 repoName := repo.Name 912 repoLastUpdated := repo.LastUpdated 913 repoDownloads := repo.DownloadCount 914 repoStars := repo.StarCount 915 repoPlatforms := repo.Platforms 916 917 row := make([]string, repoRowWidth) 918 row[repoNameIndex] = repoName 919 row[repoSizeIndex] = ellipsize(strings.ReplaceAll(humanize.Bytes(uint64(repoSize)), " ", ""), sizeWidth, ellipsis) 920 row[repoLastUpdatedIndex] = repoLastUpdated.String() 921 row[repoDownloadsIndex] = strconv.Itoa(repoDownloads) 922 row[repoStarsIndex] = strconv.Itoa(repoStars) 923 924 if verbose && len(repoPlatforms) > 0 { 925 row[repoPlatformsIndex] = getPlatformStr(repoPlatforms[0]) 926 repoPlatforms = repoPlatforms[1:] 927 } 928 929 table.Append(row) 930 931 if verbose { 932 for _, platform := range repoPlatforms { 933 row := make([]string, repoRowWidth) 934 935 row[repoPlatformsIndex] = getPlatformStr(platform) 936 937 table.Append(row) 938 } 939 } 940 941 table.Render() 942 943 return builder.String(), nil 944 } 945 946 func (repo repoStruct) stringJSON() (string, error) { 947 // Output is in json lines format - do not indent, append new line after json 948 json := jsoniter.ConfigCompatibleWithStandardLibrary 949 950 body, err := json.Marshal(repo) 951 if err != nil { 952 return "", err 953 } 954 955 return string(body) + "\n", nil 956 } 957 958 func (repo repoStruct) stringYAML() (string, error) { 959 // Output will be a multidoc yaml - use triple-dash to indicate a new document 960 body, err := yaml.Marshal(&repo) 961 if err != nil { 962 return "", err 963 } 964 965 return "---\n" + string(body), nil 966 } 967 968 type imageStruct common.ImageSummary 969 970 func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatformLen int, verbose bool) (string, error) { //nolint: lll 971 switch strings.ToLower(format) { 972 case "", defaultOutputFormat: 973 return img.stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen, verbose) 974 case jsonFormat: 975 return img.stringJSON() 976 case ymlFormat, yamlFormat: 977 return img.stringYAML() 978 default: 979 return "", zerr.ErrInvalidOutputFormat 980 } 981 } 982 983 func (img imageStruct) stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen int, verbose bool) (string, error) { 984 var builder strings.Builder 985 986 table := getImageTableWriter(&builder) 987 988 table.SetColMinWidth(colImageNameIndex, maxImgNameLen) 989 table.SetColMinWidth(colTagIndex, maxTagLen) 990 table.SetColMinWidth(colPlatformIndex, platformWidth) 991 table.SetColMinWidth(colDigestIndex, digestWidth) 992 table.SetColMinWidth(colSizeIndex, sizeWidth) 993 table.SetColMinWidth(colIsSignedIndex, isSignedWidth) 994 995 if verbose { 996 table.SetColMinWidth(colConfigIndex, configWidth) 997 table.SetColMinWidth(colLayersIndex, layersWidth) 998 } 999 1000 var imageName, tagName string 1001 1002 imageName = img.RepoName 1003 tagName = img.Tag 1004 1005 if imageNameWidth > maxImgNameLen { 1006 maxImgNameLen = imageNameWidth 1007 } 1008 1009 if tagWidth > maxTagLen { 1010 maxTagLen = tagWidth 1011 } 1012 1013 // adding spaces so that image name and tag columns are aligned 1014 // in case the name/tag are fully shown and too long 1015 var offset string 1016 if maxImgNameLen > len(imageName) { 1017 offset = strings.Repeat(" ", maxImgNameLen-len(imageName)) 1018 imageName += offset 1019 } 1020 1021 if maxTagLen > len(tagName) { 1022 offset = strings.Repeat(" ", maxTagLen-len(tagName)) 1023 tagName += offset 1024 } 1025 1026 err := addImageToTable(table, &img, maxPlatformLen, imageName, tagName, verbose) 1027 if err != nil { 1028 return "", err 1029 } 1030 1031 table.Render() 1032 1033 return builder.String(), nil 1034 } 1035 1036 func addImageToTable(table *tablewriter.Table, img *imageStruct, maxPlatformLen int, 1037 imageName, tagName string, verbose bool, 1038 ) error { 1039 switch img.MediaType { 1040 case ispec.MediaTypeImageManifest: 1041 return addManifestToTable(table, imageName, tagName, &img.Manifests[0], maxPlatformLen, verbose) 1042 case ispec.MediaTypeImageIndex: 1043 return addImageIndexToTable(table, img, maxPlatformLen, imageName, tagName, verbose) 1044 } 1045 1046 return nil 1047 } 1048 1049 func addImageIndexToTable(table *tablewriter.Table, img *imageStruct, maxPlatformLen int, 1050 imageName, tagName string, verbose bool, 1051 ) error { 1052 indexDigest, err := godigest.Parse(img.Digest) 1053 if err != nil { 1054 return fmt.Errorf("error parsing index digest %s: %w", indexDigest, err) 1055 } 1056 row := make([]string, rowWidth) 1057 row[colImageNameIndex] = imageName 1058 row[colTagIndex] = tagName 1059 row[colDigestIndex] = ellipsize(indexDigest.Encoded(), digestWidth, "") 1060 row[colPlatformIndex] = "*" 1061 1062 imgSize, _ := strconv.ParseUint(img.Size, 10, 64) 1063 row[colSizeIndex] = ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) 1064 row[colIsSignedIndex] = strconv.FormatBool(img.IsSigned) 1065 1066 if verbose { 1067 row[colConfigIndex] = "" 1068 row[colLayersIndex] = "" 1069 } 1070 1071 table.Append(row) 1072 1073 for i := range img.Manifests { 1074 err := addManifestToTable(table, "", "", &img.Manifests[i], maxPlatformLen, verbose) 1075 if err != nil { 1076 return err 1077 } 1078 } 1079 1080 return nil 1081 } 1082 1083 func addManifestToTable(table *tablewriter.Table, imageName, tagName string, manifest *common.ManifestSummary, 1084 maxPlatformLen int, verbose bool, 1085 ) error { 1086 manifestDigest, err := godigest.Parse(manifest.Digest) 1087 if err != nil { 1088 return fmt.Errorf("error parsing manifest digest %s: %w", manifest.Digest, err) 1089 } 1090 1091 configDigest, err := godigest.Parse(manifest.ConfigDigest) 1092 if err != nil { 1093 return fmt.Errorf("error parsing config digest %s: %w", manifest.ConfigDigest, err) 1094 } 1095 1096 platform := getPlatformStr(manifest.Platform) 1097 1098 if maxPlatformLen > len(platform) { 1099 offset := strings.Repeat(" ", maxPlatformLen-len(platform)) 1100 platform += offset 1101 } 1102 1103 manifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "") 1104 configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "") 1105 imgSize, _ := strconv.ParseUint(manifest.Size, 10, 64) 1106 size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) 1107 isSigned := manifest.IsSigned 1108 row := make([]string, 8) //nolint:gomnd 1109 1110 row[colImageNameIndex] = imageName 1111 row[colTagIndex] = tagName 1112 row[colDigestIndex] = manifestDigestStr 1113 row[colPlatformIndex] = platform 1114 row[colSizeIndex] = size 1115 row[colIsSignedIndex] = strconv.FormatBool(isSigned) 1116 1117 if verbose { 1118 row[colConfigIndex] = configDigestStr 1119 row[colLayersIndex] = "" 1120 } 1121 1122 table.Append(row) 1123 1124 if verbose { 1125 for _, entry := range manifest.Layers { 1126 layerSize, _ := strconv.ParseUint(entry.Size, 10, 64) 1127 size := ellipsize(strings.ReplaceAll(humanize.Bytes(layerSize), " ", ""), sizeWidth, ellipsis) 1128 1129 layerDigest, err := godigest.Parse(entry.Digest) 1130 if err != nil { 1131 return fmt.Errorf("error parsing layer digest %s: %w", entry.Digest, err) 1132 } 1133 1134 layerDigestStr := ellipsize(layerDigest.Encoded(), digestWidth, "") 1135 1136 layerRow := make([]string, 8) //nolint:gomnd 1137 layerRow[colImageNameIndex] = "" 1138 layerRow[colTagIndex] = "" 1139 layerRow[colDigestIndex] = "" 1140 layerRow[colPlatformIndex] = "" 1141 layerRow[colSizeIndex] = size 1142 layerRow[colConfigIndex] = "" 1143 layerRow[colLayersIndex] = layerDigestStr 1144 1145 table.Append(layerRow) 1146 } 1147 } 1148 1149 return nil 1150 } 1151 1152 func getPlatformStr(platform common.Platform) string { 1153 if platform.Arch == "" && platform.Os == "" { 1154 return "" 1155 } 1156 1157 fullPlatform := platform.Os 1158 1159 if platform.Arch != "" { 1160 fullPlatform = fullPlatform + "/" + platform.Arch 1161 fullPlatform = strings.Trim(fullPlatform, "/") 1162 1163 if platform.Variant != "" { 1164 fullPlatform = fullPlatform + "/" + platform.Variant 1165 } 1166 } 1167 1168 return fullPlatform 1169 } 1170 1171 func (img imageStruct) stringJSON() (string, error) { 1172 // Output is in json lines format - do not indent, append new line after json 1173 json := jsoniter.ConfigCompatibleWithStandardLibrary 1174 1175 body, err := json.Marshal(img) 1176 if err != nil { 1177 return "", err 1178 } 1179 1180 return string(body) + "\n", nil 1181 } 1182 1183 func (img imageStruct) stringYAML() (string, error) { 1184 // Output will be a multidoc yaml - use triple-dash to indicate a new document 1185 body, err := yaml.Marshal(&img) 1186 if err != nil { 1187 return "", err 1188 } 1189 1190 return "---\n" + string(body), nil 1191 } 1192 1193 type catalogResponse struct { 1194 Repositories []string `json:"repositories"` 1195 } 1196 1197 func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) { 1198 if err := validateURL(serverURL); err != nil { 1199 return "", err 1200 } 1201 1202 newURL, err := url.Parse(serverURL) 1203 if err != nil { 1204 return "", zerr.ErrInvalidURL 1205 } 1206 1207 newURL, _ = newURL.Parse(endPoint) 1208 1209 return newURL.String(), nil 1210 } 1211 1212 func ellipsize(text string, max int, trailing string) string { 1213 text = strings.TrimSpace(text) 1214 if len(text) <= max { 1215 return text 1216 } 1217 1218 chopLength := len(trailing) 1219 1220 return text[:max-chopLength] + trailing 1221 } 1222 1223 func getImageTableWriter(writer io.Writer) *tablewriter.Table { 1224 table := tablewriter.NewWriter(writer) 1225 1226 table.SetAutoWrapText(false) 1227 table.SetAutoFormatHeaders(true) 1228 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 1229 table.SetAlignment(tablewriter.ALIGN_LEFT) 1230 table.SetCenterSeparator("") 1231 table.SetColumnSeparator("") 1232 table.SetRowSeparator("") 1233 table.SetHeaderLine(false) 1234 table.SetBorder(false) 1235 table.SetTablePadding(" ") 1236 table.SetNoWhiteSpace(true) 1237 1238 return table 1239 } 1240 1241 func getCVETableWriter(writer io.Writer) *tablewriter.Table { 1242 table := tablewriter.NewWriter(writer) 1243 1244 table.SetAutoWrapText(false) 1245 table.SetAutoFormatHeaders(true) 1246 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 1247 table.SetAlignment(tablewriter.ALIGN_LEFT) 1248 table.SetCenterSeparator("") 1249 table.SetColumnSeparator("") 1250 table.SetRowSeparator("") 1251 table.SetHeaderLine(false) 1252 table.SetBorder(false) 1253 table.SetTablePadding(" ") 1254 table.SetNoWhiteSpace(true) 1255 table.SetColMinWidth(colCVEIDIndex, cveIDWidth) 1256 table.SetColMinWidth(colCVESeverityIndex, cveSeverityWidth) 1257 table.SetColMinWidth(colCVETitleIndex, cveTitleWidth) 1258 1259 return table 1260 } 1261 1262 func getReferrersTableWriter(writer io.Writer) *tablewriter.Table { 1263 table := tablewriter.NewWriter(writer) 1264 1265 table.SetAutoWrapText(false) 1266 table.SetAutoFormatHeaders(true) 1267 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 1268 table.SetAlignment(tablewriter.ALIGN_LEFT) 1269 table.SetCenterSeparator("") 1270 table.SetColumnSeparator("") 1271 table.SetRowSeparator("") 1272 table.SetHeaderLine(false) 1273 table.SetBorder(false) 1274 table.SetTablePadding(" ") 1275 table.SetNoWhiteSpace(true) 1276 1277 return table 1278 } 1279 1280 func getRepoTableWriter(writer io.Writer) *tablewriter.Table { 1281 table := tablewriter.NewWriter(writer) 1282 1283 table.SetAutoWrapText(false) 1284 table.SetAutoFormatHeaders(true) 1285 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 1286 table.SetAlignment(tablewriter.ALIGN_LEFT) 1287 table.SetCenterSeparator("") 1288 table.SetColumnSeparator("") 1289 table.SetRowSeparator("") 1290 table.SetHeaderLine(false) 1291 table.SetBorder(false) 1292 table.SetTablePadding(" ") 1293 table.SetNoWhiteSpace(true) 1294 1295 return table 1296 } 1297 1298 func (service searchService) getRepos(ctx context.Context, config SearchConfig, username, password string, 1299 rch chan stringResult, wtgrp *sync.WaitGroup, 1300 ) { 1301 defer wtgrp.Done() 1302 defer close(rch) 1303 1304 catalog := &catalogResponse{} 1305 1306 catalogEndPoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("%s%s", 1307 constants.RoutePrefix, constants.ExtCatalogPrefix)) 1308 if err != nil { 1309 if common.IsContextDone(ctx) { 1310 return 1311 } 1312 rch <- stringResult{"", err} 1313 1314 return 1315 } 1316 1317 _, err = makeGETRequest(ctx, catalogEndPoint, username, password, config.VerifyTLS, 1318 config.Debug, catalog, config.ResultWriter) 1319 if err != nil { 1320 if common.IsContextDone(ctx) { 1321 return 1322 } 1323 rch <- stringResult{"", err} 1324 1325 return 1326 } 1327 1328 fmt.Fprintln(config.ResultWriter, "\nREPOSITORY NAME") 1329 1330 if config.SortBy == SortByAlphabeticAsc { 1331 for i := 0; i < len(catalog.Repositories); i++ { 1332 fmt.Fprintln(config.ResultWriter, catalog.Repositories[i]) 1333 } 1334 } else { 1335 for i := len(catalog.Repositories) - 1; i >= 0; i-- { 1336 fmt.Fprintln(config.ResultWriter, catalog.Repositories[i]) 1337 } 1338 } 1339 } 1340 1341 const ( 1342 imageNameWidth = 10 1343 tagWidth = 8 1344 digestWidth = 8 1345 platformWidth = 14 1346 sizeWidth = 10 1347 isSignedWidth = 8 1348 downloadsWidth = 10 1349 signedWidth = 10 1350 lastUpdatedWidth = 14 1351 configWidth = 8 1352 layersWidth = 8 1353 ellipsis = "..." 1354 1355 cveIDWidth = 16 1356 cveSeverityWidth = 8 1357 cveTitleWidth = 48 1358 1359 colCVEIDIndex = 0 1360 colCVESeverityIndex = 1 1361 colCVETitleIndex = 2 1362 1363 defaultOutputFormat = "text" 1364 ) 1365 1366 const ( 1367 colImageNameIndex = iota 1368 colTagIndex 1369 colPlatformIndex 1370 colDigestIndex 1371 colConfigIndex 1372 colIsSignedIndex 1373 colLayersIndex 1374 colSizeIndex 1375 1376 rowWidth 1377 ) 1378 1379 const ( 1380 repoNameIndex = iota 1381 repoSizeIndex 1382 repoLastUpdatedIndex 1383 repoDownloadsIndex 1384 repoStarsIndex 1385 repoPlatformsIndex 1386 1387 repoRowWidth 1388 ) 1389 1390 const ( 1391 refArtifactTypeIndex = iota 1392 refSizeIndex 1393 refDigestIndex 1394 1395 refRowWidth 1396 )