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 }