zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/extensions/search/digest_test.go (about)

     1  //go:build search
     2  // +build search
     3  
     4  package search_test
     5  
     6  import (
     7  	"encoding/json"
     8  	"net/url"
     9  	"os"
    10  	"testing"
    11  	"time"
    12  
    13  	godigest "github.com/opencontainers/go-digest"
    14  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    15  	. "github.com/smartystreets/goconvey/convey"
    16  	"gopkg.in/resty.v1"
    17  
    18  	"zotregistry.io/zot/pkg/api"
    19  	"zotregistry.io/zot/pkg/api/config"
    20  	"zotregistry.io/zot/pkg/api/constants"
    21  	"zotregistry.io/zot/pkg/common"
    22  	extconf "zotregistry.io/zot/pkg/extensions/config"
    23  	. "zotregistry.io/zot/pkg/test/common"
    24  	"zotregistry.io/zot/pkg/test/deprecated"
    25  	. "zotregistry.io/zot/pkg/test/image-utils"
    26  )
    27  
    28  type ImgResponseForDigest struct {
    29  	ImgListForDigest ImgListForDigest  `json:"data"`
    30  	Errors           []common.ErrorGQL `json:"errors"`
    31  }
    32  
    33  //nolint:tagliatelle // graphQL schema
    34  type ImgListForDigest struct {
    35  	PaginatedImagesResultForDigest `json:"ImageListForDigest"`
    36  }
    37  
    38  //nolint:tagliatelle // graphQL schema
    39  type ImgInfo struct {
    40  	RepoName     string `json:"RepoName"`
    41  	Tag          string `json:"Tag"`
    42  	ConfigDigest string `json:"ConfigDigest"`
    43  	Digest       string `json:"Digest"`
    44  	Size         string `json:"Size"`
    45  }
    46  
    47  type PaginatedImagesResultForDigest struct {
    48  	Results []ImgInfo       `json:"results"`
    49  	Page    common.PageInfo `json:"page"`
    50  }
    51  
    52  func TestDigestSearchHTTP(t *testing.T) {
    53  	Convey("Test image search by digest scanning", t, func() {
    54  		rootDir := t.TempDir()
    55  
    56  		port := GetFreePort()
    57  		baseURL := GetBaseURL(port)
    58  		conf := config.New()
    59  		conf.HTTP.Port = port
    60  		conf.Storage.RootDirectory = rootDir
    61  		defaultVal := true
    62  		conf.Extensions = &extconf.ExtensionConfig{
    63  			Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
    64  		}
    65  
    66  		ctlr := api.NewController(conf)
    67  		ctrlManager := NewControllerManager(ctlr)
    68  
    69  		ctrlManager.StartAndWait(port)
    70  
    71  		// shut down server
    72  		defer ctrlManager.StopServer()
    73  
    74  		createdTime1 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC)
    75  		layers1 := [][]byte{
    76  			{3, 2, 2},
    77  		}
    78  		image1, err := deprecated.GetImageWithComponents( //nolint: staticcheck
    79  			ispec.Image{
    80  				Created: &createdTime1,
    81  				History: []ispec.History{
    82  					{
    83  						Created: &createdTime1,
    84  					},
    85  				},
    86  			},
    87  			layers1,
    88  		)
    89  		So(err, ShouldBeNil)
    90  
    91  		const ver001 = "0.0.1"
    92  
    93  		err = UploadImage(image1, baseURL, "zot-cve-test", ver001)
    94  		So(err, ShouldBeNil)
    95  
    96  		createdTime2 := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC)
    97  		image2, err := deprecated.GetImageWithComponents( //nolint: staticcheck
    98  			ispec.Image{
    99  				History: []ispec.History{{Created: &createdTime2}},
   100  				Platform: ispec.Platform{
   101  					Architecture: "amd64",
   102  					OS:           "linux",
   103  				},
   104  			},
   105  			[][]byte{
   106  				{0, 0, 2},
   107  			},
   108  		)
   109  		So(err, ShouldBeNil)
   110  
   111  		manifestDigest := image2.Digest()
   112  
   113  		err = UploadImage(image2, baseURL, "zot-test", ver001)
   114  		So(err, ShouldBeNil)
   115  
   116  		configBlob, err := json.Marshal(image2.Config)
   117  		So(err, ShouldBeNil)
   118  
   119  		configDigest := godigest.FromBytes(configBlob)
   120  
   121  		resp, err := resty.R().Get(baseURL + "/v2/")
   122  		So(resp, ShouldNotBeNil)
   123  		So(err, ShouldBeNil)
   124  		So(resp.StatusCode(), ShouldEqual, 200)
   125  
   126  		resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix)
   127  		So(resp, ShouldNotBeNil)
   128  		So(err, ShouldBeNil)
   129  		So(resp.StatusCode(), ShouldEqual, 422)
   130  
   131  		// "sha" should match all digests in all images
   132  		query := `{
   133  			ImageListForDigest(id:"sha") {
   134  				Results {
   135  					RepoName Tag 
   136  					Manifests {
   137  						Digest ConfigDigest Size 
   138  						Layers { Digest }
   139  					}
   140  					Size
   141  				}
   142  			}
   143  		}`
   144  		resp, err = resty.R().Get(
   145  			baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
   146  		)
   147  		So(resp, ShouldNotBeNil)
   148  		So(err, ShouldBeNil)
   149  		So(resp.StatusCode(), ShouldEqual, 200)
   150  
   151  		var responseStruct ImgResponseForDigest
   152  		err = json.Unmarshal(resp.Body(), &responseStruct)
   153  		So(err, ShouldBeNil)
   154  		So(len(responseStruct.Errors), ShouldEqual, 0)
   155  		So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 2)
   156  		So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
   157  
   158  		// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
   159  		// GetTestBlobDigest("zot-test", "manifest").Encoded() should match the manifest of 1 image
   160  
   161  		gqlQuery := url.QueryEscape(`{ImageListForDigest(id:"` + manifestDigest.Encoded() + `")
   162  			{Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`)
   163  		targetURL := baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery
   164  
   165  		resp, err = resty.R().Get(targetURL)
   166  		So(string(resp.Body()), ShouldNotBeNil)
   167  		So(resp, ShouldNotBeNil)
   168  		So(err, ShouldBeNil)
   169  		So(resp.StatusCode(), ShouldEqual, 200)
   170  
   171  		err = json.Unmarshal(resp.Body(), &responseStruct)
   172  		So(err, ShouldBeNil)
   173  		So(len(responseStruct.Errors), ShouldEqual, 0)
   174  		So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 1)
   175  		So(responseStruct.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-test")
   176  		So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
   177  
   178  		gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + configDigest.Encoded() + `")
   179  		{Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`)
   180  
   181  		targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery
   182  		resp, err = resty.R().Get(targetURL)
   183  
   184  		So(resp, ShouldNotBeNil)
   185  		So(err, ShouldBeNil)
   186  		So(resp.StatusCode(), ShouldEqual, 200)
   187  
   188  		err = json.Unmarshal(resp.Body(), &responseStruct)
   189  		So(err, ShouldBeNil)
   190  		So(len(responseStruct.Errors), ShouldEqual, 0)
   191  		So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 1)
   192  		So(responseStruct.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-test")
   193  		So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
   194  
   195  		// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}}
   196  		// GetTestBlobDigest("zot-cve-test", "layer").Encoded() should match the layer of 1 image
   197  		layerDigest1 := godigest.FromBytes((layers1[0]))
   198  		gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + layerDigest1.Encoded() + `")
   199  		{Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`)
   200  		targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery
   201  
   202  		resp, err = resty.R().Get(
   203  			targetURL,
   204  		)
   205  
   206  		So(resp, ShouldNotBeNil)
   207  		So(err, ShouldBeNil)
   208  		So(resp.StatusCode(), ShouldEqual, 200)
   209  		var responseStruct2 ImgResponseForDigest
   210  
   211  		err = json.Unmarshal(resp.Body(), &responseStruct2)
   212  		So(err, ShouldBeNil)
   213  		So(len(responseStruct2.Errors), ShouldEqual, 0)
   214  		So(len(responseStruct2.ImgListForDigest.Results), ShouldEqual, 1)
   215  		So(responseStruct2.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-cve-test")
   216  		So(responseStruct2.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
   217  
   218  		// Call should return {"data":{"ImageListForDigest":[]}}
   219  		// "1111111" should match 0 images
   220  		query = `
   221  		{
   222  			ImageListForDigest(id:"1111111") {
   223  				Results {				
   224  					RepoName Tag 
   225  					Manifests {
   226  						Digest ConfigDigest Size 
   227  						Layers { Digest }
   228  					}
   229  				}
   230  			}
   231  		}`
   232  		resp, err = resty.R().Get(
   233  			baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
   234  		)
   235  		So(resp, ShouldNotBeNil)
   236  		So(err, ShouldBeNil)
   237  		So(resp.StatusCode(), ShouldEqual, 200)
   238  
   239  		err = json.Unmarshal(resp.Body(), &responseStruct)
   240  		So(err, ShouldBeNil)
   241  		So(len(responseStruct.Errors), ShouldEqual, 0)
   242  		So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 0)
   243  
   244  		// Call should return {"errors": [{....}]", data":null}}
   245  		query = `{
   246  			ImageListForDigest(id:"1111111") {
   247  				Results {
   248  					RepoName Tag343s
   249  				}
   250  			}`
   251  		resp, err = resty.R().Get(
   252  			baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
   253  		)
   254  		So(resp, ShouldNotBeNil)
   255  		So(err, ShouldBeNil)
   256  		So(resp.StatusCode(), ShouldEqual, 422)
   257  
   258  		err = json.Unmarshal(resp.Body(), &responseStruct)
   259  		So(err, ShouldBeNil)
   260  		So(len(responseStruct.Errors), ShouldEqual, 1)
   261  	})
   262  }
   263  
   264  func TestDigestSearchHTTPSubPaths(t *testing.T) {
   265  	Convey("Test image search by digest scanning using storage subpaths", t, func() {
   266  		subRootDir := t.TempDir()
   267  
   268  		port := GetFreePort()
   269  		baseURL := GetBaseURL(port)
   270  		conf := config.New()
   271  		conf.HTTP.Port = port
   272  		defaultVal := true
   273  		conf.Extensions = &extconf.ExtensionConfig{
   274  			Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
   275  		}
   276  
   277  		ctlr := api.NewController(conf)
   278  
   279  		globalDir := t.TempDir()
   280  		defer os.RemoveAll(globalDir)
   281  
   282  		ctlr.Config.Storage.RootDirectory = globalDir
   283  
   284  		subPathMap := make(map[string]config.StorageConfig)
   285  
   286  		subPathMap["/a"] = config.StorageConfig{RootDirectory: subRootDir}
   287  
   288  		ctlr.Config.Storage.SubPaths = subPathMap
   289  		ctrlManager := NewControllerManager(ctlr)
   290  
   291  		ctrlManager.StartAndWait(port)
   292  
   293  		// shut down server
   294  		defer ctrlManager.StopServer()
   295  
   296  		config, layers, manifest, err := deprecated.GetImageComponents(100) //nolint: staticcheck
   297  		So(err, ShouldBeNil)
   298  
   299  		err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "a/zot-cve-test", "0.0.1")
   300  		So(err, ShouldBeNil)
   301  
   302  		err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "a/zot-test", "0.0.1")
   303  		So(err, ShouldBeNil)
   304  
   305  		resp, err := resty.R().Get(baseURL + "/v2/")
   306  		So(resp, ShouldNotBeNil)
   307  		So(err, ShouldBeNil)
   308  		So(resp.StatusCode(), ShouldEqual, 200)
   309  
   310  		resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix)
   311  		So(resp, ShouldNotBeNil)
   312  		So(err, ShouldBeNil)
   313  		So(resp.StatusCode(), ShouldEqual, 422)
   314  
   315  		query := `{
   316  			ImageListForDigest(id:"sha") {
   317  				Results {
   318  					RepoName Tag 
   319  					Manifests {
   320  						Digest ConfigDigest Size 
   321  						Layers { Digest }
   322  						}
   323  					}
   324  				}
   325  			}`
   326  		resp, err = resty.R().Get(
   327  			baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
   328  		)
   329  		So(resp, ShouldNotBeNil)
   330  		So(err, ShouldBeNil)
   331  		So(resp.StatusCode(), ShouldEqual, 200)
   332  
   333  		var responseStruct ImgResponseForDigest
   334  		err = json.Unmarshal(resp.Body(), &responseStruct)
   335  		So(err, ShouldBeNil)
   336  		So(len(responseStruct.Errors), ShouldEqual, 0)
   337  		So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 2)
   338  	})
   339  }
   340  
   341  func TestDigestSearchDisabled(t *testing.T) {
   342  	Convey("Test disabling image search", t, func() {
   343  		var disabled bool
   344  		port := GetFreePort()
   345  		baseURL := GetBaseURL(port)
   346  		conf := config.New()
   347  		conf.HTTP.Port = port
   348  		conf.Storage.RootDirectory = t.TempDir()
   349  		conf.Extensions = &extconf.ExtensionConfig{
   350  			Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &disabled}},
   351  		}
   352  
   353  		ctlr := api.NewController(conf)
   354  		ctrlManager := NewControllerManager(ctlr)
   355  
   356  		ctrlManager.StartAndWait(port)
   357  
   358  		// shut down server
   359  		defer ctrlManager.StopServer()
   360  
   361  		resp, err := resty.R().Get(baseURL + "/v2/")
   362  		So(resp, ShouldNotBeNil)
   363  		So(err, ShouldBeNil)
   364  		So(resp.StatusCode(), ShouldEqual, 200)
   365  
   366  		resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix)
   367  		So(resp, ShouldNotBeNil)
   368  		So(err, ShouldBeNil)
   369  		So(resp.StatusCode(), ShouldEqual, 404)
   370  	})
   371  }