github.com/quay/claircore@v1.5.28/aws/client.go (about)

     1  package aws
     2  
     3  import (
     4  	"compress/gzip"
     5  	"context"
     6  	"encoding/xml"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"path"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/quay/zlog"
    17  
    18  	"github.com/quay/claircore/aws/internal/alas"
    19  	"github.com/quay/claircore/internal/xmlutil"
    20  	"github.com/quay/claircore/pkg/tmp"
    21  )
    22  
    23  const (
    24  	repoDataPath     = "/repodata/repomd.xml"
    25  	updatesPath      = "/repodata/updateinfo.xml.gz"
    26  	defaultOpTimeout = 15 * time.Second
    27  )
    28  
    29  // Client is an http for accessing ALAS mirrors.
    30  type Client struct {
    31  	c       *http.Client
    32  	mirrors []*url.URL
    33  }
    34  
    35  func NewClient(ctx context.Context, hc *http.Client, release Release) (*Client, error) {
    36  	ctx = zlog.ContextWithValues(ctx, "release", string(release))
    37  	if hc == nil {
    38  		return nil, errors.New("http.Client not provided")
    39  	}
    40  	client := &Client{
    41  		c:       hc,
    42  		mirrors: []*url.URL{},
    43  	}
    44  	tctx, cancel := context.WithTimeout(ctx, defaultOpTimeout)
    45  	defer cancel()
    46  	err := client.getMirrors(tctx, release.mirrorlist())
    47  	return client, err
    48  }
    49  
    50  // RepoMD returns a alas.RepoMD containing sha256 information of a repositories contents
    51  func (c *Client) RepoMD(ctx context.Context) (alas.RepoMD, error) {
    52  	ctx = zlog.ContextWithValues(ctx, "component", "aws/Client.RepoMD")
    53  	for _, mirror := range c.mirrors {
    54  		m := *mirror
    55  		m.Path = path.Join(m.Path, repoDataPath)
    56  		ctx := zlog.ContextWithValues(ctx, "mirror", m.String())
    57  
    58  		req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.String(), nil)
    59  		if err != nil {
    60  			zlog.Error(ctx).Err(err).Msg("failed to make request object")
    61  			continue
    62  		}
    63  
    64  		zlog.Debug(ctx).Msg("attempting repomd download")
    65  		resp, err := c.c.Do(req)
    66  		if err != nil {
    67  			zlog.Error(ctx).Err(err).Msg("failed to retrieve repomd")
    68  			continue
    69  		}
    70  		defer resp.Body.Close()
    71  
    72  		switch resp.StatusCode {
    73  		case http.StatusOK:
    74  			// break
    75  		default:
    76  			zlog.Error(ctx).
    77  				Int("code", resp.StatusCode).
    78  				Str("status", resp.Status).
    79  				Msg("unexpected HTTP response")
    80  			continue
    81  		}
    82  
    83  		repoMD := alas.RepoMD{}
    84  		dec := xml.NewDecoder(resp.Body)
    85  		dec.CharsetReader = xmlutil.CharsetReader
    86  		if err := dec.Decode(&repoMD); err != nil {
    87  			zlog.Error(ctx).
    88  				Err(err).
    89  				Msg("failed xml unmarshal")
    90  			continue
    91  		}
    92  
    93  		zlog.Debug(ctx).Msg("success")
    94  		return repoMD, nil
    95  	}
    96  
    97  	zlog.Error(ctx).Msg("exhausted all mirrors")
    98  	return alas.RepoMD{}, fmt.Errorf("all mirrors failed to retrieve repo metadata")
    99  }
   100  
   101  // Updates returns the *http.Response of the first mirror to establish a connection
   102  func (c *Client) Updates(ctx context.Context) (io.ReadCloser, error) {
   103  	ctx = zlog.ContextWithValues(ctx, "component", "aws/Client.Updates")
   104  	for _, mirror := range c.mirrors {
   105  		m := *mirror
   106  		m.Path = path.Join(m.Path, updatesPath)
   107  		ctx := zlog.ContextWithValues(ctx, "mirror", m.String())
   108  
   109  		req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.String(), nil)
   110  		if err != nil {
   111  			zlog.Error(ctx).Err(err).Msg("failed to make request object")
   112  			continue
   113  		}
   114  
   115  		tf, err := tmp.NewFile("", "")
   116  		if err != nil {
   117  			zlog.Error(ctx).Err(err).Msg("failed to open temp file")
   118  			continue
   119  		}
   120  		var success bool
   121  		defer func() {
   122  			if !success {
   123  				if err := tf.Close(); err != nil {
   124  					zlog.Warn(ctx).Err(err).Msg("unable to close spool")
   125  				}
   126  			}
   127  		}()
   128  
   129  		resp, err := c.c.Do(req)
   130  		if err != nil {
   131  			zlog.Error(ctx).Err(err).Msg("failed to retrieve updates")
   132  			continue
   133  		}
   134  		defer resp.Body.Close()
   135  
   136  		switch resp.StatusCode {
   137  		case http.StatusOK:
   138  			// break
   139  		default:
   140  			zlog.Error(ctx).
   141  				Int("code", resp.StatusCode).
   142  				Str("status", resp.Status).
   143  				Msg("unexpected HTTP response")
   144  			continue
   145  		}
   146  
   147  		if _, err := io.Copy(tf, resp.Body); err != nil {
   148  			return nil, err
   149  		}
   150  		if o, err := tf.Seek(0, io.SeekStart); err != nil || o != 0 {
   151  			return nil, err
   152  		}
   153  		gz, err := gzip.NewReader(tf)
   154  		if err != nil {
   155  			return nil, fmt.Errorf("failed to create gzip reader: %v", err)
   156  		}
   157  
   158  		zlog.Debug(ctx).Msg("success")
   159  		success = true
   160  		return &gzippedFile{
   161  			Reader: gz,
   162  			Closer: tf,
   163  		}, nil
   164  	}
   165  
   166  	zlog.Error(ctx).Msg("exhausted all mirrors")
   167  	return nil, fmt.Errorf("all update_info mirrors failed to return a response")
   168  }
   169  
   170  // gzippedFile implements io.ReadCloser by proxying calls to different
   171  // underlying implementations. This is used to make sure the file that backs the
   172  // downloaded security database has Close called on it.
   173  type gzippedFile struct {
   174  	io.Reader
   175  	io.Closer
   176  }
   177  
   178  func (c *Client) getMirrors(ctx context.Context, list string) error {
   179  	ctx = zlog.ContextWithValues(ctx, "component", "aws/Client.getMirrors")
   180  
   181  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, list, nil)
   182  	if err != nil {
   183  		return fmt.Errorf("failed to create request for mirror list: %v", err)
   184  	}
   185  	resp, err := c.c.Do(req)
   186  	if err != nil {
   187  		return fmt.Errorf("failed to make request for mirrors: %v", err)
   188  	}
   189  	defer resp.Body.Close()
   190  
   191  	switch resp.StatusCode {
   192  	case http.StatusOK:
   193  		// break
   194  	default:
   195  		return fmt.Errorf("failed to make request for mirrors: unexpected response %d %s", resp.StatusCode, resp.Status)
   196  	}
   197  
   198  	if err := ctx.Err(); err != nil {
   199  		return err
   200  	}
   201  
   202  	body, err := io.ReadAll(resp.Body)
   203  	if err != nil {
   204  		return fmt.Errorf("failed to read http body: %v", err)
   205  	}
   206  
   207  	b := strings.TrimSuffix(string(body), "\n")
   208  	urls := strings.Split(b, "\n")
   209  
   210  	for _, u := range urls {
   211  		uu, err := url.Parse(u)
   212  		if err != nil {
   213  			return fmt.Errorf("could not parse returned mirror %v as URL: %v", u, err)
   214  		}
   215  		c.mirrors = append(c.mirrors, uu)
   216  	}
   217  
   218  	zlog.Debug(ctx).
   219  		Msg("successfully got list of mirrors")
   220  	return nil
   221  }