blob: 2c8bcdf2179200d1e2ef53cdfd8d3c23e24099b2 [file] [log] [blame]
Serge Bazanski77b87a62023-04-03 15:24:27 +02001package main
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "fmt"
8 "io"
9 "log"
10 "net/http"
11 "net/url"
12 "path/filepath"
13 "strings"
14 "time"
15
16 "cloud.google.com/go/storage"
17 "github.com/cenkalti/backoff/v4"
18 "k8s.io/klog/v2"
19)
20
21// rpmDef is a definition of an RPM dependency, containing the internal
22// bazeldnf/bazel name of the dependency, an expected SHA256 sum of the RPM file,
23// and a list of URLs of where that file should be downloaded from. This
24// structure is parsed from repositories.bzl.
25type rpmDef struct {
26 name string
27 sha256 string
28 mpath string
29 urls []*url.URL
30}
31
32// newRPMDef builds and validates an rpmDef based on raw data from
33// repositories.bzl.
34func newRPMDef(name string, sha256 string, urls []string) (*rpmDef, error) {
35 if len(urls) < 1 {
36 return nil, fmt.Errorf("needs at least one URL")
37 }
38 var urlsP []*url.URL
39
40 // Look through all URLs and make sure they're valid Fedora mirror paths, and
41 // that all the mirror paths are the same.
42 path := ""
43 for _, us := range urls {
44 u, err := url.Parse(us)
45 if err != nil {
46 return nil, fmt.Errorf("url invalid %w", err)
47 }
48
49 mpath, err := getFedoraMirrorPath(u)
50 if err != nil {
51 return nil, fmt.Errorf("unexpected url %s: %w", us, err)
52 }
53
54 // If this isn't the first mirror path we've seen, make sure they're the same.
55 if path == "" {
56 path = mpath
57 } else {
58 if path != mpath {
59 return nil, fmt.Errorf("url path difference, %s vs %s", path, mpath)
60 }
61 }
62 urlsP = append(urlsP, u)
63 }
64 return &rpmDef{
65 name: name,
66 sha256: sha256,
67 urls: urlsP,
68 mpath: path,
69 }, nil
70}
71
72// getFedoraMirrorPath takes a full URL to a mirrored RPM and returns its
73// mirror-root-relative path, ie. the path which starts with fedora/linux/....
74func getFedoraMirrorPath(u *url.URL) (string, error) {
75 parts := strings.Split(u.Path, "/")
76
77 // Find fedora/linux/...
78 found := false
79 for i, p := range parts {
80 if p == "fedora" && (i+1) < len(parts) && parts[i+1] == "linux" {
81 parts = parts[i:]
82 found = true
83 break
84 }
85 }
86 if !found || len(parts) < 7 {
87 return "", fmt.Errorf("does not look like a fedora mirror URL")
88 }
89 // Make sure the rest of the path makes some vague sense.
90 switch parts[2] {
91 case "releases", "updates":
92 default:
93 return "", fmt.Errorf("unexpected category %q", parts[2])
94 }
95 switch parts[4] {
96 case "Everything":
97 default:
98 return "", fmt.Errorf("unexpected category %q", parts[3])
99 }
100 switch parts[5] {
101 case "x86_64":
102 default:
103 return "", fmt.Errorf("unexpected architecture %q", parts[5])
104 }
105
106 // Return the path rebuilt and starting at fedora/linux/...
107 return strings.Join(parts, "/"), nil
108}
109
110// validateOurs checks if our mirror has a copy of this RPM. If deep is true, the
111// file will be downloaded and its SHA256 verified. Otherwise, a simple HEAD
112// request is used.
113func (e *rpmDef) validateOurs(ctx context.Context, deep bool) (bool, error) {
114 ctxT, ctxC := context.WithTimeout(ctx, 2*time.Second)
115 defer ctxC()
116
117 url := ourMirrorURL(e.mpath)
118
119 bo := backoff.NewExponentialBackOff()
120 var found bool
121 err := backoff.Retry(func() error {
122 method := "HEAD"
123 if deep {
124 method = "GET"
125 }
126 req, err := http.NewRequestWithContext(ctxT, method, url, nil)
127 if err != nil {
128 return backoff.Permanent(err)
129 }
130 res, err := http.DefaultClient.Do(req)
131 if err != nil {
132 return err
133 }
134 defer res.Body.Close()
135 if res.StatusCode == 200 {
136 found = true
137 } else {
138 found = false
139 }
140
141 if !deep || !found {
142 return nil
143 }
144
145 data, err := io.ReadAll(res.Body)
146 if err != nil {
147 return err
148 }
149
150 h := sha256.New()
151 h.Write(data)
152 got := hex.EncodeToString(h.Sum(nil))
153 want := strings.ToLower(e.sha256)
154 if want != got {
155 log.Printf("SHA256 mismatch: wanted %s, got %s", want, got)
156 found = false
157 }
158 return nil
159
160 }, backoff.WithContext(bo, ctxT))
161 if err != nil {
162 return false, err
163 }
164 return found, nil
165}
166
167// mirrorToOurs attempts to download this RPM from a mirror that's not ours and
168// upload it to our mirror via the given bucket.
169func (e *rpmDef) mirrorToOurs(ctx context.Context, bucket *storage.BucketHandle) error {
170 log.Printf("Mirroring %s ...", e.name)
171 for _, source := range e.urls {
172 // Skip our own mirror as a source.
173 if strings.HasPrefix(source.String(), ourMirrorURL()) {
174 continue
175 }
176
177 log.Printf(" Getting %s ...", source)
178 data, err := e.get(ctx, source.String())
179 if err != nil {
180 klog.Errorf(" Failed: %v", err)
181 continue
182 }
183
184 objName := filepath.Join(flagMirrorBucketSubdir, e.mpath)
185 obj := bucket.Object(objName)
186 log.Printf(" Uploading to %s...", objName)
187 wr := obj.NewWriter(ctx)
188 if _, err := wr.Write(data); err != nil {
189 return fmt.Errorf("Write failed: %w", err)
190 }
191 if err := wr.Close(); err != nil {
192 return fmt.Errorf("Close failed: %w", err)
193 }
194 return nil
195 }
196 return fmt.Errorf("all mirrors failed")
197}
198
199// get downloads the given RPM from the given URL and checks its SHA256.
200func (e *rpmDef) get(ctx context.Context, url string) ([]byte, error) {
201 ctxT, ctxC := context.WithTimeout(ctx, 60*time.Second)
202 defer ctxC()
203
204 bo := backoff.NewExponentialBackOff()
205 var data []byte
206 err := backoff.Retry(func() error {
207 req, err := http.NewRequestWithContext(ctxT, "GET", url, nil)
208 if err != nil {
209 return backoff.Permanent(err)
210 }
211 res, err := http.DefaultClient.Do(req)
212 if err != nil {
213 return err
214 }
215 defer res.Body.Close()
216 data, err = io.ReadAll(res.Body)
217 if err != nil {
218 return err
219 }
220 return nil
221 }, backoff.WithContext(bo, ctxT))
222 if err != nil {
223 return nil, err
224 }
225
226 h := sha256.New()
227 h.Write(data)
228 got := hex.EncodeToString(h.Sum(nil))
229 want := strings.ToLower(e.sha256)
230 if want != got {
231 return nil, fmt.Errorf("sha256 mismatch: wanted %s, got %s", want, got)
232 }
233 return data, nil
234}