github.com/quay/claircore@v1.5.28/test/fetch/layer.go (about) 1 // Package fetch implements just enough of a client for the OCI Distribution 2 // specification for use in tests. 3 package fetch 4 5 import ( 6 "compress/gzip" 7 "context" 8 "encoding/hex" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "net/http" 14 "net/url" 15 "os" 16 "path" 17 "path/filepath" 18 "strings" 19 "sync" 20 "testing" 21 22 "github.com/quay/claircore" 23 "github.com/quay/claircore/test/integration" 24 ) 25 26 const ( 27 ua = `claircore/test/fetch` 28 ) 29 30 var registry = map[string]*client{ 31 "docker.io": {Root: "https://registry-1.docker.io/"}, 32 "gcr.io": {Root: "https://gcr.io/"}, 33 "ghcr.io": {Root: "https://ghcr.io/"}, 34 "quay.io": {Root: "https://quay.io/"}, 35 "registry.access.redhat.com": {Root: "https://registry.access.redhat.com/"}, 36 } 37 38 var pkgClient = &http.Client{ 39 Transport: &http.Transport{}, 40 } 41 42 // Layer returns the specified layer contents, cached in the global layer cache. 43 func Layer(ctx context.Context, t testing.TB, from, repo string, blob claircore.Digest, opt ...Option) (*os.File, error) { 44 t.Helper() 45 opts := make(map[Option]bool) 46 for _, o := range opt { 47 opts[o] = true 48 } 49 // Splitting the digest like this future-proofs things against some weirdo 50 // running this on Windows. 51 cachefile := filepath.Join(integration.CacheDir(t), "layer", blob.Algorithm(), hex.EncodeToString(blob.Checksum())) 52 switch _, err := os.Stat(cachefile); { 53 case err == nil: 54 t.Logf("layer cached: %s", cachefile) 55 return os.Open(cachefile) 56 case errors.Is(err, os.ErrNotExist) && opts[IgnoreIntegration]: 57 case errors.Is(err, os.ErrNotExist): 58 // need to do work 59 integration.Skip(t) 60 default: 61 return nil, err 62 } 63 checkpath.Do(func() { 64 if err := os.MkdirAll(filepath.Dir(cachefile), 0o755); err != nil { 65 t.Errorf("unable to create needed directories: %v", err) 66 } 67 }) 68 t.Logf("fetching layer into: %s", cachefile) 69 70 client, ok := registry[from] 71 if !ok { 72 return nil, fmt.Errorf("unknown registry: %q", from) 73 } 74 rc, err := client.Blob(ctx, pkgClient, repo, blob) 75 if err != nil { 76 return nil, err 77 } 78 defer rc.Close() 79 // BUG(hank) Any compression scheme that isn't gzip isn't handled correctly. 80 if !opts[NoDecompression] { 81 var gr *gzip.Reader 82 gr, err = gzip.NewReader(rc) 83 if err != nil { 84 return nil, err 85 } 86 defer gr.Close() 87 rc = gr 88 } 89 cf := copyTo(t, cachefile, rc) 90 if t.Failed() { 91 os.Remove(cachefile) 92 return nil, errors.New("unable to open cachefile") 93 } 94 return cf, nil 95 } 96 97 // Option is options for [Fetch] 98 type Option uint 99 100 const ( 101 _ Option = iota 102 // IgnoreIntegration causes [Fetch] to use the network unconditionally. 103 IgnoreIntegration 104 // NoDecompression does no compression. 105 NoDecompression 106 ) 107 108 // Checkpath guards against making the same directory over and over. 109 var checkpath sync.Once 110 111 func copyTo(t testing.TB, name string, rc io.Reader) *os.File { 112 cf, err := os.Create(name) 113 if err != nil { 114 t.Error(err) 115 return nil 116 } 117 t.Cleanup(func() { 118 if err := cf.Close(); err != nil { 119 t.Log(err) 120 } 121 }) 122 123 if _, err = io.Copy(cf, rc); err != nil { 124 t.Error(err) 125 return nil 126 } 127 if err = cf.Sync(); err != nil { 128 t.Error(err) 129 return nil 130 } 131 if _, err = cf.Seek(0, io.SeekStart); err != nil { 132 t.Error(err) 133 return nil 134 } 135 136 return cf 137 } 138 139 type tokenResponse struct { 140 Token string `json:"token"` 141 } 142 143 // Client is a more generic registry client. 144 type client struct { 145 tokCache sync.Map 146 Root string 147 } 148 149 func (d *client) getToken(repo string) string { 150 if v, ok := d.tokCache.Load(repo); ok { 151 return v.(string) 152 } 153 return "" 154 } 155 156 func (d *client) putToken(repo, tok string) { 157 d.tokCache.Store(repo, tok) 158 } 159 160 func (d *client) doAuth(ctx context.Context, c *http.Client, name, h string) error { 161 if !strings.HasPrefix(h, `Bearer `) { 162 return errors.New("weird header") 163 } 164 attrs := map[string]string{} 165 fs := strings.Split(strings.TrimPrefix(h, `Bearer `), ",") 166 for _, f := range fs { 167 i := strings.IndexByte(f, '=') 168 if i == -1 { 169 return errors.New("even weirder header") 170 } 171 k := f[:i] 172 v := strings.Trim(f[i+1:], `"`) 173 attrs[k] = v 174 } 175 176 // Request a token 177 u, err := url.Parse(attrs["realm"]) 178 if err != nil { 179 return err 180 } 181 v := url.Values{ 182 "service": {attrs["service"]}, 183 "scope": {attrs["scope"]}, 184 } 185 u.RawQuery = v.Encode() 186 req := &http.Request{ 187 ProtoMajor: 1, 188 ProtoMinor: 1, 189 Proto: "HTTP/1.1", 190 URL: u, 191 Host: u.Host, 192 Method: http.MethodGet, 193 Header: http.Header{"User-Agent": {ua}}, 194 } 195 res, err := c.Do(req.WithContext(ctx)) 196 if err != nil { 197 return err 198 } 199 switch res.StatusCode { 200 case http.StatusOK: 201 default: 202 return fmt.Errorf("%s %v: %v", req.Method, req.URL, res.Status) 203 } 204 defer res.Body.Close() 205 var tok tokenResponse 206 if err := json.NewDecoder(res.Body).Decode(&tok); err != nil { 207 return err 208 } 209 d.putToken(name, "Bearer "+tok.Token) 210 return nil 211 } 212 213 func (d *client) Blob(ctx context.Context, c *http.Client, name string, blob claircore.Digest) (io.ReadCloser, error) { 214 u, err := url.Parse(d.Root) 215 if err != nil { 216 return nil, err 217 } 218 u, err = u.Parse(path.Join("v2", name, "blobs", blob.String())) 219 if err != nil { 220 return nil, err 221 } 222 req := &http.Request{ 223 ProtoMajor: 1, 224 ProtoMinor: 1, 225 URL: u, 226 Host: u.Host, 227 Method: http.MethodGet, 228 Header: http.Header{"User-Agent": {ua}}, 229 } 230 if h := d.getToken(name); h != "" { 231 req.Header.Set(`authorization`, h) 232 } 233 res, err := c.Do(req.WithContext(ctx)) 234 if err != nil { 235 return nil, err 236 } 237 238 switch res.StatusCode { 239 case http.StatusOK: 240 case http.StatusUnauthorized: 241 auth := res.Header.Get(`www-authenticate`) 242 if err := d.doAuth(ctx, c, name, auth); err != nil { 243 return nil, err 244 } 245 return d.Blob(ctx, c, name, blob) 246 default: 247 return nil, fmt.Errorf("%s %v: %v", req.Method, req.URL, res.Status) 248 } 249 return res.Body, nil 250 }