github.com/quay/claircore@v1.5.28/updater/offline_v1.go (about)

     1  package updater
     2  
     3  import (
     4  	"archive/zip"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"os"
    12  	"path"
    13  	"runtime"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/google/uuid"
    18  	"github.com/quay/zlog"
    19  	"golang.org/x/sync/errgroup"
    20  
    21  	"github.com/quay/claircore/updater/driver/v1"
    22  )
    23  
    24  /*
    25  This file implements the V1 import/export format, aka zip-of-zips.
    26  
    27  The config is written to the root at "config.json", which should allow the Parse
    28  step to ensure it has the same configuration parameters that the Fetch step did.
    29  
    30  All Updaters created at the Fetch step have their data recorded in
    31  subdirectories keyed by name.
    32  */
    33  
    34  const exportV1 = `1`
    35  
    36  func (u *Updater) importV1(ctx context.Context, sys fs.FS) error {
    37  	var cfg driver.Configs
    38  	f, err := sys.Open("config.json")
    39  	if err != nil {
    40  		return fmt.Errorf("updater import: %w", err)
    41  	}
    42  	defer func() {
    43  		if err := f.Close(); err != nil {
    44  			zlog.Warn(ctx).Err(err).Msg("error closing config.json")
    45  		}
    46  	}()
    47  	if err := json.NewDecoder(f).Decode(&cfg); err != nil {
    48  		return err
    49  	}
    50  
    51  	us, err := u.updaters(ctx, cfg)
    52  	if err != nil {
    53  		return err
    54  	}
    55  	zlog.Info(ctx).
    56  		Int("ct", len(us)).
    57  		Msg("got updaters")
    58  
    59  	spool, err := os.CreateTemp(tmpDir, tmpPattern)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	defer func() {
    64  		spoolname := spool.Name()
    65  		if err := os.Remove(spoolname); err != nil {
    66  			zlog.Warn(ctx).Str("filename", spoolname).Err(err).Msg("unable to remove spool file")
    67  		}
    68  		if err := spool.Close(); err != nil {
    69  			zlog.Warn(ctx).Str("filename", spoolname).Err(err).Msg("error closing spool file")
    70  		}
    71  	}()
    72  
    73  	for _, upd := range us {
    74  		name := upd.Name
    75  		ctx := zlog.ContextWithValues(ctx, "updater", name)
    76  		fi, err := fs.Stat(sys, name)
    77  		switch {
    78  		case errors.Is(err, nil):
    79  		case errors.Is(err, fs.ErrNotExist):
    80  			zlog.Info(ctx).
    81  				Msg("no import, skipping")
    82  			continue
    83  		default:
    84  			return err
    85  		}
    86  		if !fi.IsDir() {
    87  			return errors.New("malformed input")
    88  		}
    89  		if _, err := spool.Seek(0, io.SeekStart); err != nil {
    90  			return err
    91  		}
    92  		f, err := sys.Open(path.Join(name, `data`))
    93  		if err != nil {
    94  			return err
    95  		}
    96  		sz, err := io.Copy(spool, f)
    97  		f.Close()
    98  		if err != nil {
    99  			return err
   100  		}
   101  		z, err := zip.NewReader(spool, sz)
   102  		if err != nil {
   103  			return err
   104  		}
   105  
   106  		res, err := u.parseOne(ctx, upd, z)
   107  		if err != nil {
   108  			return err
   109  		}
   110  
   111  		b, err := fs.ReadFile(sys, path.Join(name, `fingerprint`))
   112  		if err != nil {
   113  			return err
   114  		}
   115  		fp := driver.Fingerprint(b)
   116  
   117  		var ref uuid.UUID
   118  		b, err = fs.ReadFile(sys, path.Join(name, `ref`))
   119  		if err != nil {
   120  			return err
   121  		}
   122  		if err := ref.UnmarshalText(b); err != nil {
   123  			return err
   124  		}
   125  
   126  		// Load into DB.
   127  		if len(res.Vulnerabilities.Vulnerability) != 0 {
   128  			if err := u.store.UpdateVulnerabilities(ctx, ref, name, fp, res.Vulnerabilities); err != nil {
   129  				return err
   130  			}
   131  			zlog.Info(ctx).Stringer("ref", ref).Msg("updated vulnerabilites")
   132  		}
   133  		if len(res.Enrichments) != 0 {
   134  			if err := u.store.UpdateEnrichments(ctx, ref, name, fp, res.Enrichments); err != nil {
   135  				return err
   136  			}
   137  			zlog.Info(ctx).Stringer("ref", ref).Msg("updated enrichments")
   138  		}
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  func (u *Updater) exportV1(ctx context.Context, z *zip.Writer, prev fs.FS) error {
   145  	now := time.Now()
   146  
   147  	w, err := z.CreateHeader(&zip.FileHeader{
   148  		Name:     "config.json",
   149  		Comment:  "updater configuration from the producer",
   150  		Modified: now,
   151  		Method:   zstdCompression,
   152  	})
   153  	if err != nil {
   154  		return err
   155  	}
   156  	if err := json.NewEncoder(w).Encode(u.configs); err != nil {
   157  		return err
   158  	}
   159  
   160  	us, err := u.updaters(ctx, u.configs)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	zlog.Info(ctx).
   165  		Int("ct", len(us)).
   166  		Msg("got updaters")
   167  
   168  	// WaitGroup for the worker goroutines.
   169  	var wg sync.WaitGroup
   170  	lim := runtime.GOMAXPROCS(0)
   171  	wg.Add(lim)
   172  	pfps := make(map[string]driver.Fingerprint)
   173  	if prev != nil {
   174  		walk := func(p string, _ fs.DirEntry, err error) error {
   175  			if err != nil {
   176  				return err
   177  			}
   178  			dir, base := path.Split(p)
   179  			if dir == "." || base != "fingerprint" {
   180  				// Don't bother with anything that's not the fingerprint file.
   181  				return nil
   182  			}
   183  
   184  			b, err := fs.ReadFile(prev, p)
   185  			if err != nil {
   186  				return err
   187  			}
   188  			pfps[dir] = driver.Fingerprint(b)
   189  			return nil
   190  		}
   191  		if err := fs.WalkDir(prev, ".", walk); err != nil {
   192  			return err
   193  		}
   194  	}
   195  	// ErrGroup for the workers, feeder, and closer goroutines.
   196  	eg, ctx := errgroup.WithContext(ctx)
   197  	feed, res := make(chan taggedUpdater), make(chan *result)
   198  
   199  	eg.Go(feeder(ctx, feed, us))
   200  	// Closer goroutine.
   201  	eg.Go(func() error {
   202  		wg.Wait()
   203  		close(res)
   204  		return nil
   205  	})
   206  	eg.Go(func() error {
   207  		// Collect results and write them out to the zip at "z".
   208  		for r := range res {
   209  			if err := addUpdater(ctx, z, now, r); err != nil {
   210  				return err
   211  			}
   212  		}
   213  		return nil
   214  	})
   215  	for i := 0; i < lim; i++ {
   216  		// Worker goroutine.
   217  		eg.Go(func() error {
   218  			defer wg.Done()
   219  			for upd := range feed {
   220  				name := upd.Name
   221  				ctx := zlog.ContextWithValues(ctx, "updater", name)
   222  				// Zips have to be written serially, so we spool the fetcher
   223  				// output to disk, then seek back to the start so it's ready to
   224  				// read.
   225  				//
   226  				// Make sure to close the file in any error cases. The log
   227  				// prints here use "info" for the application errors and "warn"
   228  				// for the OS errors that really shouldn't be happening but we
   229  				// can't do much to recover from.
   230  				spool, err := os.CreateTemp(tmpDir, tmpPattern)
   231  				if err != nil {
   232  					zlog.Warn(ctx).Err(err).Msg("unable to create spool file")
   233  					continue
   234  				}
   235  				spoolname := spool.Name()
   236  				fp, err := u.fetchOne(ctx, upd, pfps[name], spool)
   237  				if err != nil {
   238  					if err := os.Remove(spoolname); err != nil {
   239  						zlog.Warn(ctx).Str("filename", spoolname).Err(err).Msg("unable to remove spool file")
   240  					}
   241  					if err := spool.Close(); err != nil {
   242  						zlog.Warn(ctx).Str("filename", spoolname).Err(err).Msg("error closing spool file")
   243  					}
   244  					zlog.Info(ctx).Err(err).Msg("updater error")
   245  					continue
   246  				}
   247  				if _, err := spool.Seek(0, io.SeekStart); err != nil {
   248  					if err := os.Remove(spoolname); err != nil {
   249  						zlog.Warn(ctx).Str("filename", spoolname).Err(err).Msg("unable to remove spool file")
   250  					}
   251  					if err := spool.Close(); err != nil {
   252  						zlog.Warn(ctx).Str("filename", spoolname).Err(err).Msg("error closing spool file")
   253  					}
   254  					zlog.Warn(ctx).Str("filename", spoolname).Err(err).Msg("unable to seek to start")
   255  					continue
   256  				}
   257  				res <- &result{
   258  					fp:    fp,
   259  					spool: spool,
   260  					name:  name,
   261  				}
   262  			}
   263  			return nil
   264  		})
   265  	}
   266  
   267  	return eg.Wait()
   268  }
   269  
   270  // AddUpdater writes the results to the zip "z", recording it as time "now".
   271  //
   272  // This function assumes the result isn't in an error state.
   273  func addUpdater(ctx context.Context, z *zip.Writer, now time.Time, r *result) error {
   274  	defer func() {
   275  		fn := r.spool.Name()
   276  		if err := os.Remove(fn); err != nil {
   277  			zlog.Warn(ctx).Err(err).Str("file", fn).Msg("unable to remove fetch spool")
   278  		}
   279  		if err := r.spool.Close(); err != nil {
   280  			zlog.Warn(ctx).Err(err).Str("file", fn).Msg("unable to close fetch spool")
   281  		}
   282  	}()
   283  	n := r.name
   284  	// Create a dir entry just to preserve filesystem semantics. Makes
   285  	// things easier to use upon import.
   286  	if _, err := z.Create(n + "/"); err != nil {
   287  		return err
   288  	}
   289  	// Write Fingerprint
   290  	w, err := z.CreateHeader(&zip.FileHeader{
   291  		Name:     path.Join(n, `fingerprint`),
   292  		Modified: now,
   293  		Method:   zip.Deflate,
   294  	})
   295  	if err != nil {
   296  		return err
   297  	}
   298  	if _, err := w.Write([]byte(r.fp)); err != nil {
   299  		return err
   300  	}
   301  	// Write a ref
   302  	w, err = z.CreateHeader(&zip.FileHeader{
   303  		Name:     path.Join(n, `ref`),
   304  		Modified: now,
   305  		Method:   zip.Store,
   306  	})
   307  	if err != nil {
   308  		return err
   309  	}
   310  	ref := uuid.New()
   311  	b, err := ref.MarshalText()
   312  	if err != nil {
   313  		return err
   314  	}
   315  	if _, err := w.Write(b); err != nil {
   316  		return err
   317  	}
   318  	// Write data
   319  	w, err = z.CreateHeader(&zip.FileHeader{
   320  		Name:     path.Join(n, `data`),
   321  		Modified: now,
   322  		Method:   zstdCompression,
   323  	})
   324  	if err != nil {
   325  		return err
   326  	}
   327  	if _, err := io.Copy(w, r.spool); err != nil {
   328  		return err
   329  	}
   330  	zlog.Debug(ctx).
   331  		Stringer("ref", ref).
   332  		Str("name", n).
   333  		Msg("wrote out fetch results")
   334  	return nil
   335  }
   336  
   337  type result struct {
   338  	spool *os.File
   339  	fp    driver.Fingerprint
   340  	name  string
   341  }