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