|  | 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 | 
|  | } |