github.com/quay/claircore@v1.5.28/test/periodic/rpm_test.go (about)

     1  package periodic
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"path"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/quay/zlog"
    16  
    17  	"github.com/quay/claircore"
    18  	"github.com/quay/claircore/rpm"
    19  	"github.com/quay/claircore/test"
    20  	"github.com/quay/claircore/test/fetch"
    21  	"github.com/quay/claircore/test/rpmtest"
    22  )
    23  
    24  // TestRPMSpotCheck searches against the production Hydra API to find published
    25  // container images, then fetches and indexes the manifest and compares it to
    26  // the published RPM manifest for the image.
    27  func TestRPMSpotCheck(t *testing.T) {
    28  	ctx := context.Background()
    29  	// This is the URL for our search query. Needs to get Solr search parameters
    30  	// added to the RawQuery member.
    31  	query := url.URL{
    32  		Scheme: "https",
    33  		Host:   "access.redhat.com",
    34  		Path:   "/hydra/rest/search/kcs",
    35  	}
    36  	for _, pair := range [][2]string{
    37  		{"ubi", "repository:ubi?/ubi*"},
    38  		{"s2i", "repository:ubi?/s2i-*"},
    39  		{"nodejs", "repository:ubi?/nodejs*"},
    40  	} {
    41  		query := query
    42  		// This is Solr search values. Need to add an `fq` and `q` parameter to use.
    43  		qv := url.Values{
    44  			"redhat_client": {"claircore-tests"},
    45  			"fq": {
    46  				`documentKind:"ContainerRepository"`,
    47  				`-eol_date:[* TO NOW]`,
    48  			},
    49  			"fl":   {"id,repository,registry,parsed_data_layers"},
    50  			"rows": {"500"},
    51  		}
    52  		qv.Set("q", pair[0])
    53  		qv.Add("fq", pair[1])
    54  		query.RawQuery = qv.Encode()
    55  		t.Run(pair[0], func(t *testing.T) {
    56  			t.Parallel()
    57  			req, err := http.NewRequestWithContext(ctx, http.MethodGet, query.String(), nil)
    58  			if err != nil {
    59  				t.Fatal(err)
    60  			}
    61  			req.Header.Set("accept", "application/json")
    62  			res, err := pkgClient.Do(req)
    63  			if err != nil {
    64  				t.Fatal(err)
    65  			}
    66  			defer res.Body.Close()
    67  			if res.StatusCode != http.StatusOK {
    68  				t.Fatalf("unexpected response to %q: %s", query.String(), res.Status)
    69  			}
    70  			t.Logf("%s: %s", query.String(), res.Status)
    71  			var searchRes hydraResponse
    72  			var buf bytes.Buffer
    73  			if err := json.NewDecoder(io.TeeReader(res.Body, &buf)).Decode(&searchRes); err != nil {
    74  				t.Error(err)
    75  			}
    76  			defer func() {
    77  				if !t.Failed() {
    78  					return
    79  				}
    80  				t.Logf("search response:\t%q", buf.String())
    81  			}()
    82  			dir := t.TempDir()
    83  			for _, d := range searchRes.Response.Docs {
    84  				t.Run(d.Repository, d.Run(dir))
    85  			}
    86  		})
    87  	}
    88  }
    89  
    90  type hydraResponse struct {
    91  	Response struct {
    92  		Docs []hydraDoc `json:"docs"`
    93  	} `json:"response"`
    94  }
    95  
    96  type hydraDoc struct {
    97  	ID         string             `json:"id"`
    98  	Repository string             `json:"repository"`
    99  	Registry   string             `json:"registry"`
   100  	Layers     []claircore.Digest `json:"parsed_data_layers"`
   101  }
   102  
   103  type imageInfo struct {
   104  	Links imageInfoLinks `json:"_links"`
   105  }
   106  
   107  type imageInfoLinks struct {
   108  	Images      link `json:"images"`
   109  	RpmManifest link `json:"rpm_manifest"`
   110  }
   111  
   112  type link struct {
   113  	Href string `json:"href"`
   114  }
   115  
   116  type imagesResponse struct {
   117  	Data []struct {
   118  		ID     string         `json:"_id"`
   119  		Links  imageInfoLinks `json:"_links"`
   120  		Parsed struct {
   121  			Layers []claircore.Digest `json:"layers"`
   122  		} `json:"parsed_data"`
   123  	} `json:"data"`
   124  }
   125  
   126  func (doc hydraDoc) Run(dir string) func(*testing.T) {
   127  	root := url.URL{
   128  		Scheme: "https",
   129  		Host:   "catalog.redhat.com",
   130  		Path:   "/api/containers/",
   131  	}
   132  	return func(t *testing.T) {
   133  		t.Parallel()
   134  		ctx := zlog.Test(context.Background(), t)
   135  		try := 1
   136  
   137  	Retry:
   138  		fetchURL, err := root.Parse(path.Join("/api/containers/", "v1/repositories/id/", doc.ID))
   139  		if err != nil {
   140  			t.Fatal(err)
   141  		}
   142  		req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL.String(), nil)
   143  		if err != nil {
   144  			t.Fatal(err)
   145  		}
   146  		req.Header.Set("accept", "application/json")
   147  		res, err := pkgClient.Do(req)
   148  		if err != nil {
   149  			t.Fatal(err)
   150  		}
   151  		defer res.Body.Close()
   152  		switch res.StatusCode {
   153  		case http.StatusOK:
   154  		case http.StatusServiceUnavailable:
   155  			if try == 10 {
   156  				t.Fatal("too many retries")
   157  			}
   158  			time.Sleep(time.Duration(try*2) * time.Second)
   159  			try++
   160  			goto Retry
   161  		default:
   162  			t.Fatalf("unexpected response to %q: %s", fetchURL.String(), res.Status)
   163  		}
   164  		buf := &bytes.Buffer{}
   165  		var info imageInfo
   166  		if err := json.NewDecoder(io.TeeReader(res.Body, buf)).Decode(&info); err != nil {
   167  			t.Fatalf("%s: %v", fetchURL.String(), err)
   168  		}
   169  		defer logResponse(t, res.Request.URL.Path, buf)()
   170  
   171  		imageURL, err := root.Parse(path.Join("/api/containers/", info.Links.Images.Href))
   172  		if err != nil {
   173  			t.Fatal(err)
   174  		}
   175  		imageURL.RawQuery = (url.Values{
   176  			"page_size": {"1"},
   177  			"page":      {"0"},
   178  			"exclude":   {"data.repositories.comparison.advisory_rpm_mapping,data.brew,data.cpe_ids,data.top_layer_id"},
   179  			"filter":    {"deleted!=true"},
   180  		}).Encode()
   181  		req, err = http.NewRequestWithContext(ctx, http.MethodGet, imageURL.String(), nil)
   182  		if err != nil {
   183  			t.Fatal(err)
   184  		}
   185  		req.Header.Set("accept", "application/json")
   186  		res, err = pkgClient.Do(req)
   187  		if err != nil {
   188  			t.Fatal(err)
   189  		}
   190  		defer res.Body.Close()
   191  		if res.StatusCode != http.StatusOK {
   192  			t.Fatalf("unexpected response to %q: %s", imageURL.String(), res.Status)
   193  		}
   194  		buf = &bytes.Buffer{}
   195  		var image imagesResponse
   196  		if err := json.NewDecoder(io.TeeReader(res.Body, buf)).Decode(&image); err != nil {
   197  			t.Fatalf("%s: %v", imageURL.String(), err)
   198  		}
   199  		defer logResponse(t, res.Request.URL.Path, buf)()
   200  
   201  		manifestURL, err := fetchURL.Parse(path.Join("/api/containers/", image.Data[0].Links.RpmManifest.Href))
   202  		if err != nil {
   203  			t.Fatal(err)
   204  		}
   205  		req, err = http.NewRequestWithContext(ctx, http.MethodGet, manifestURL.String(), nil)
   206  		if err != nil {
   207  			t.Fatal(err)
   208  		}
   209  		req.Header.Set("accept", "application/json")
   210  		res, err = pkgClient.Do(req)
   211  		if err != nil {
   212  			t.Fatal(err)
   213  		}
   214  		defer res.Body.Close()
   215  		if res.StatusCode != http.StatusOK {
   216  			t.Fatalf("unexpected response to %q: %s", manifestURL.String(), res.Status)
   217  		}
   218  
   219  		buf = &bytes.Buffer{}
   220  		want := rpmtest.PackagesFromRPMManifest(t, io.TeeReader(res.Body, buf))
   221  		defer logResponse(t, res.Request.URL.Path, buf)()
   222  
   223  		s := &rpm.Scanner{}
   224  		var got []*claircore.Package
   225  		var which claircore.Digest
   226  		for _, ld := range image.Data[0].Parsed.Layers {
   227  			// TODO(hank) Need a way to use the nicer API, but pass the
   228  			// Integration bypass.
   229  			n, err := fetch.Layer(ctx, t, doc.Registry, doc.Repository, ld, fetch.IgnoreIntegration)
   230  			if err != nil {
   231  				t.Fatal(err)
   232  			}
   233  			defer n.Close()
   234  			var l claircore.Layer
   235  			if err := l.Init(ctx, &test.AnyDescription, n); err != nil {
   236  				t.Fatal(err)
   237  			}
   238  			defer l.Close()
   239  			l.Hash = ld // If you're reading this for an example of how to work with layers: don't do this.
   240  
   241  			pkgs, err := s.Scan(ctx, &l)
   242  			if err != nil {
   243  				t.Error(err)
   244  			}
   245  			if len(pkgs) >= len(want) {
   246  				got = pkgs
   247  				which = ld
   248  				break
   249  			}
   250  		}
   251  		t.Logf("found %d packages in %v", len(got), which)
   252  		t.Logf("comparing to %d packages in manifest %s", len(want), doc.ID)
   253  
   254  		if !cmp.Equal(got, want, rpmtest.Options) {
   255  			t.Error(cmp.Diff(got, want, rpmtest.Options))
   256  		}
   257  	}
   258  }
   259  
   260  func logResponse(t *testing.T, u string, b *bytes.Buffer) func() {
   261  	return func() {
   262  		if !t.Failed() {
   263  			return
   264  		}
   265  		t.Logf("%s response:\t%q", u, b.String())
   266  	}
   267  }