| package main |
| |
| import ( |
| "context" |
| "crypto/sha256" |
| "encoding/hex" |
| "fmt" |
| "io" |
| "log" |
| "net/http" |
| "net/url" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "cloud.google.com/go/storage" |
| "github.com/cenkalti/backoff/v4" |
| "k8s.io/klog/v2" |
| ) |
| |
| // rpmDef is a definition of an RPM dependency, containing the internal |
| // bazeldnf/bazel name of the dependency, an expected SHA256 sum of the RPM file, |
| // and a list of URLs of where that file should be downloaded from. This |
| // structure is parsed from repositories.bzl. |
| type rpmDef struct { |
| name string |
| sha256 string |
| mpath string |
| urls []*url.URL |
| } |
| |
| // newRPMDef builds and validates an rpmDef based on raw data from |
| // repositories.bzl. |
| func newRPMDef(name string, sha256 string, urls []string) (*rpmDef, error) { |
| if len(urls) < 1 { |
| return nil, fmt.Errorf("needs at least one URL") |
| } |
| var urlsP []*url.URL |
| |
| // Look through all URLs and make sure they're valid Fedora mirror paths, and |
| // that all the mirror paths are the same. |
| path := "" |
| for _, us := range urls { |
| u, err := url.Parse(us) |
| if err != nil { |
| return nil, fmt.Errorf("url invalid %w", err) |
| } |
| |
| mpath, err := getFedoraMirrorPath(u) |
| if err != nil { |
| return nil, fmt.Errorf("unexpected url %s: %w", us, err) |
| } |
| |
| // If this isn't the first mirror path we've seen, make sure they're the same. |
| if path == "" { |
| path = mpath |
| } else { |
| if path != mpath { |
| return nil, fmt.Errorf("url path difference, %s vs %s", path, mpath) |
| } |
| } |
| urlsP = append(urlsP, u) |
| } |
| return &rpmDef{ |
| name: name, |
| sha256: sha256, |
| urls: urlsP, |
| mpath: path, |
| }, nil |
| } |
| |
| // getFedoraMirrorPath takes a full URL to a mirrored RPM and returns its |
| // mirror-root-relative path, ie. the path which starts with fedora/linux/.... |
| func getFedoraMirrorPath(u *url.URL) (string, error) { |
| parts := strings.Split(u.Path, "/") |
| |
| // Find fedora/linux/... |
| found := false |
| for i, p := range parts { |
| if p == "fedora" && (i+1) < len(parts) && parts[i+1] == "linux" { |
| parts = parts[i:] |
| found = true |
| break |
| } |
| } |
| if !found || len(parts) < 7 { |
| return "", fmt.Errorf("does not look like a fedora mirror URL") |
| } |
| // Make sure the rest of the path makes some vague sense. |
| switch parts[2] { |
| case "releases", "updates": |
| default: |
| return "", fmt.Errorf("unexpected category %q", parts[2]) |
| } |
| switch parts[4] { |
| case "Everything": |
| default: |
| return "", fmt.Errorf("unexpected category %q", parts[3]) |
| } |
| switch parts[5] { |
| case "x86_64": |
| default: |
| return "", fmt.Errorf("unexpected architecture %q", parts[5]) |
| } |
| |
| // Return the path rebuilt and starting at fedora/linux/... |
| return strings.Join(parts, "/"), nil |
| } |
| |
| // validateOurs checks if our mirror has a copy of this RPM. If deep is true, the |
| // file will be downloaded and its SHA256 verified. Otherwise, a simple HEAD |
| // request is used. |
| func (e *rpmDef) validateOurs(ctx context.Context, deep bool) (bool, error) { |
| ctxT, ctxC := context.WithTimeout(ctx, 2*time.Second) |
| defer ctxC() |
| |
| url := ourMirrorURL(e.mpath) |
| |
| bo := backoff.NewExponentialBackOff() |
| var found bool |
| err := backoff.Retry(func() error { |
| method := "HEAD" |
| if deep { |
| method = "GET" |
| } |
| req, err := http.NewRequestWithContext(ctxT, method, url, nil) |
| if err != nil { |
| return backoff.Permanent(err) |
| } |
| res, err := http.DefaultClient.Do(req) |
| if err != nil { |
| return err |
| } |
| defer res.Body.Close() |
| if res.StatusCode == 200 { |
| found = true |
| } else { |
| found = false |
| } |
| |
| if !deep || !found { |
| return nil |
| } |
| |
| data, err := io.ReadAll(res.Body) |
| if err != nil { |
| return err |
| } |
| |
| h := sha256.New() |
| h.Write(data) |
| got := hex.EncodeToString(h.Sum(nil)) |
| want := strings.ToLower(e.sha256) |
| if want != got { |
| log.Printf("SHA256 mismatch: wanted %s, got %s", want, got) |
| found = false |
| } |
| return nil |
| |
| }, backoff.WithContext(bo, ctxT)) |
| if err != nil { |
| return false, err |
| } |
| return found, nil |
| } |
| |
| // mirrorToOurs attempts to download this RPM from a mirror that's not ours and |
| // upload it to our mirror via the given bucket. |
| func (e *rpmDef) mirrorToOurs(ctx context.Context, bucket *storage.BucketHandle) error { |
| log.Printf("Mirroring %s ...", e.name) |
| for _, source := range e.urls { |
| // Skip our own mirror as a source. |
| if strings.HasPrefix(source.String(), ourMirrorURL()) { |
| continue |
| } |
| |
| log.Printf(" Getting %s ...", source) |
| data, err := e.get(ctx, source.String()) |
| if err != nil { |
| klog.Errorf(" Failed: %v", err) |
| continue |
| } |
| |
| objName := filepath.Join(flagMirrorBucketSubdir, e.mpath) |
| obj := bucket.Object(objName) |
| log.Printf(" Uploading to %s...", objName) |
| wr := obj.NewWriter(ctx) |
| if _, err := wr.Write(data); err != nil { |
| return fmt.Errorf("Write failed: %w", err) |
| } |
| if err := wr.Close(); err != nil { |
| return fmt.Errorf("Close failed: %w", err) |
| } |
| return nil |
| } |
| return fmt.Errorf("all mirrors failed") |
| } |
| |
| // get downloads the given RPM from the given URL and checks its SHA256. |
| func (e *rpmDef) get(ctx context.Context, url string) ([]byte, error) { |
| ctxT, ctxC := context.WithTimeout(ctx, 60*time.Second) |
| defer ctxC() |
| |
| bo := backoff.NewExponentialBackOff() |
| var data []byte |
| err := backoff.Retry(func() error { |
| req, err := http.NewRequestWithContext(ctxT, "GET", url, nil) |
| if err != nil { |
| return backoff.Permanent(err) |
| } |
| res, err := http.DefaultClient.Do(req) |
| if err != nil { |
| return err |
| } |
| defer res.Body.Close() |
| data, err = io.ReadAll(res.Body) |
| if err != nil { |
| return err |
| } |
| return nil |
| }, backoff.WithContext(bo, ctxT)) |
| if err != nil { |
| return nil, err |
| } |
| |
| h := sha256.New() |
| h.Write(data) |
| got := hex.EncodeToString(h.Sum(nil)) |
| want := strings.ToLower(e.sha256) |
| if want != got { |
| return nil, fmt.Errorf("sha256 mismatch: wanted %s, got %s", want, got) |
| } |
| return data, nil |
| } |