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  }