github.com/quay/claircore@v1.5.28/alpine/updater.go (about) 1 package alpine 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "net/http" 8 "net/url" 9 "path" 10 "strings" 11 "sync" 12 13 "github.com/quay/zlog" 14 15 "github.com/quay/claircore/libvuln/driver" 16 ) 17 18 //doc:url updater 19 const dbURL = "https://secdb.alpinelinux.org/" 20 21 type updater struct { 22 client *http.Client 23 release release 24 repo string 25 url string 26 } 27 28 var ( 29 _ driver.Updater = (*updater)(nil) 30 _ driver.Configurable = (*updater)(nil) 31 _ driver.UpdaterSetFactory = (*Factory)(nil) 32 _ driver.Configurable = (*Factory)(nil) 33 ) 34 35 // Factory is an UpdaterSetFactory for ingesting an Alpine SecDB. 36 // 37 // Factory expects to be able to discover a directory layout like the one at [https://secdb.alpinelinux.org/] at the configured URL. 38 // More explictly, it expects: 39 // - a "last-update" file with opaque contents that change when any constituent database changes 40 // - contiguously numbered directories with the name "v$maj.$min" starting with "maj" as "3" and "min" as at most "3" 41 // - JSON files inside those directories named "main.json" or "community.json" 42 // 43 // The [Configure] method must be called before the [UpdaterSet] method. 44 type Factory struct { 45 c *http.Client 46 base *url.URL 47 48 mu sync.Mutex 49 stamp []byte 50 etag string 51 cur driver.UpdaterSet 52 } 53 54 // NewFactory returns a constructed Factory. 55 // 56 // [Configure] must still be called before [UpdaterSet]. 57 func NewFactory(_ context.Context) (*Factory, error) { 58 return &Factory{}, nil 59 } 60 61 // UpdaterSet implements driver.UpdaterSetFactory. 62 func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) { 63 ctx = zlog.ContextWithValues(ctx, "component", "alpine/Factory.UpdaterSet") 64 s := driver.NewUpdaterSet() 65 if f.c == nil { 66 zlog.Info(ctx). 67 Msg("unconfigured") 68 return s, nil 69 } 70 71 u, err := f.base.Parse("last-update") 72 if err != nil { 73 return s, fmt.Errorf("alpine: unable to construct request: %w", err) 74 } 75 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) 76 if err != nil { 77 return s, fmt.Errorf("alpine: unable to construct request: %w", err) 78 } 79 f.mu.Lock() 80 defer f.mu.Unlock() 81 if f.etag != "" { 82 req.Header.Set(`if-none-match`, f.etag) 83 } 84 zlog.Debug(ctx). 85 Stringer("url", u). 86 Msg("making request") 87 res, err := f.c.Do(req) 88 if err != nil { 89 return s, fmt.Errorf("alpine: error requesting %q: %w", u.String(), err) 90 } 91 defer res.Body.Close() 92 switch res.StatusCode { 93 case http.StatusNotModified: 94 zlog.Debug(ctx). 95 Stringer("url", u). 96 Msg("not modified") 97 return f.cur, nil 98 case http.StatusOK: 99 default: 100 return s, fmt.Errorf("alpine: unexpected status requesting `last-update`: %v", res.Status) 101 } 102 var b bytes.Buffer 103 if _, err := b.ReadFrom(res.Body); err != nil { 104 return s, fmt.Errorf("alpine: error requesting `last-update`: %w", err) 105 } 106 if bytes.Equal(f.stamp, b.Bytes()) { 107 return f.cur, nil 108 } 109 newStamp := make([]byte, b.Len()) 110 copy(newStamp, b.Bytes()) 111 b.Reset() 112 newEtag := res.Header.Get("etag") 113 114 var todo []release 115 Major: 116 for maj := 3; ; maj++ { 117 foundLower := false 118 min := 0 119 if maj == 3 { 120 // Start at v3.3. The previous version of the code didn't handle v3.2. 121 min = 3 122 } 123 Minor: 124 for ; ; min++ { 125 r := stableRelease{maj, min} 126 u, err := f.base.Parse(r.String() + "/") 127 if err != nil { 128 return s, fmt.Errorf("alpine: unable to construct request: %w", err) 129 } 130 ctx := zlog.ContextWithValues(ctx, "url", u.String(), "release", r.String()) 131 req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil) 132 if err != nil { 133 return s, fmt.Errorf("alpine: unable to construct request: %w", err) 134 } 135 zlog.Debug(ctx).Msg("checking release") 136 res, err := f.c.Do(req) 137 if err != nil { 138 return s, fmt.Errorf("alpine: error requesting %q: %w", u.String(), err) 139 } 140 res.Body.Close() 141 switch res.StatusCode { 142 case http.StatusOK: 143 foundLower = true 144 todo = append(todo, r) 145 case http.StatusNotFound: 146 zlog.Debug(ctx).Msg("not found") 147 if foundLower { 148 break Minor 149 } 150 break Major 151 default: 152 zlog.Info(ctx).Str("status", res.Status).Msg("unexpected status reported") 153 } 154 } 155 } 156 for _, r := range append(todo, edgeRelease{}) { 157 for _, n := range []string{`main`, `community`} { 158 u, err := f.base.Parse(path.Join(r.String(), n+".json")) 159 if err != nil { 160 return s, fmt.Errorf("alpine: unable to construct request: %w", err) 161 } 162 ctx := zlog.ContextWithValues(ctx, "url", u.String(), "release", r.String(), "repo", n) 163 req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil) 164 if err != nil { 165 return s, fmt.Errorf("alpine: unable to construct request: %w", err) 166 } 167 zlog.Debug(ctx).Msg("checking repository") 168 res, err := f.c.Do(req) 169 if err != nil { 170 return s, fmt.Errorf("alpine: error requesting %q: %w", u.String(), err) 171 } 172 res.Body.Close() 173 switch res.StatusCode { 174 case http.StatusOK: 175 zlog.Debug(ctx).Msg("found") 176 case http.StatusNotFound: 177 zlog.Debug(ctx).Msg("not found") 178 continue 179 default: 180 zlog.Info(ctx).Str("status", res.Status).Msg("unexpected status reported") 181 continue 182 } 183 s.Add(&updater{ 184 repo: n, 185 release: r, // NB: Safe to copy because it's an array or empty struct. 186 url: u.String(), 187 }) 188 } 189 } 190 191 f.etag = newEtag 192 f.stamp = newStamp 193 f.cur = s 194 return s, nil 195 } 196 197 // FactoryConfig is the configuration accepted by the Factory. 198 // 199 // By convention, this is keyed by the string "alpine". 200 type FactoryConfig struct { 201 // URL indicates the base URL for the SecDB layout. It should have a trailing slash. 202 URL string `json:"url" yaml:"url"` 203 } 204 205 // Configure implements driver.Configurable. 206 func (f *Factory) Configure(ctx context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error { 207 f.c = c 208 var cfg FactoryConfig 209 if err := cf(&cfg); err != nil { 210 return err 211 } 212 var err error 213 u := dbURL 214 if cfg.URL != "" { 215 u = cfg.URL 216 if !strings.HasSuffix(u, "/") { 217 u += "/" 218 } 219 } 220 f.base, err = url.Parse(u) 221 if err != nil { 222 return err 223 } 224 return nil 225 } 226 227 func (u *updater) Name() string { 228 return fmt.Sprintf("alpine-%s-%s-updater", u.repo, u.release) 229 } 230 231 // UpdaterConfig is the configuration accepted by Alpine updaters. 232 // 233 // By convention, this should be in a map called "alpine-${REPO}-${RELEASE}-updater". 234 // For example, "alpine-main-v3.12-updater". 235 // 236 // If a SecDB JSON file is not found at the proper place by [Factory.UpdaterSet], this configuration will not be consulted. 237 type UpdaterConfig struct { 238 // URL overrides any discovered URL for the JSON file. 239 URL string `json:"url" yaml:"url"` 240 } 241 242 // Configure implements driver.Configurable. 243 func (u *updater) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error { 244 var cfg UpdaterConfig 245 if err := f(&cfg); err != nil { 246 return err 247 } 248 if cfg.URL != "" { 249 u.url = cfg.URL 250 zlog.Info(ctx). 251 Str("component", "alpine/Updater.Configure"). 252 Str("updater", u.Name()). 253 Msg("configured url") 254 } 255 u.client = c 256 return nil 257 }