From 050bc2d1f44824f2573c6219a21d3a00e1ce7a76 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Fri, 28 Aug 2020 13:41:21 -0400 Subject: [PATCH] clairctl: add import and export commands This commit brings airgap updater support into clair. Signed-off-by: Hank Donnay --- cmd/clairctl/config.go | 23 +++++++++ cmd/clairctl/export.go | 103 +++++++++++++++++++++++++++++++++++++++++ cmd/clairctl/import.go | 93 +++++++++++++++++++++++++++++++++++++ cmd/clairctl/main.go | 2 + 4 files changed, 221 insertions(+) create mode 100644 cmd/clairctl/config.go create mode 100644 cmd/clairctl/export.go create mode 100644 cmd/clairctl/import.go diff --git a/cmd/clairctl/config.go b/cmd/clairctl/config.go new file mode 100644 index 0000000000..5e8088c610 --- /dev/null +++ b/cmd/clairctl/config.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + + "gopkg.in/yaml.v3" + + "github.com/quay/clair/v4/config" +) + +func loadConfig(n string) (*config.Config, error) { + f, err := os.Open(n) + if err != nil { + return nil, err + } + defer f.Close() + + var cfg config.Config + if err := yaml.NewDecoder(f).Decode(&cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/cmd/clairctl/export.go b/cmd/clairctl/export.go new file mode 100644 index 0000000000..de4e6fe5d6 --- /dev/null +++ b/cmd/clairctl/export.go @@ -0,0 +1,103 @@ +package main + +import ( + "errors" + "io" + "os" + "regexp" + + "github.com/quay/claircore/libvuln" + "github.com/quay/claircore/libvuln/driver" + "github.com/quay/claircore/updater" + _ "github.com/quay/claircore/updater/defaults" + "github.com/urfave/cli/v2" +) + +// ExportCmd is the "export-updaters" subcommand. +var ExportCmd = &cli.Command{ + Name: "export-updaters", + Description: "Run configured exporters and export to a file.", + Action: exportAction, + Usage: "run updaters and export results", + ArgsUsage: "[out]", + Flags: []cli.Flag{ + // Strict can be used to check that updaters still work. + &cli.BoolFlag{ + Name: "strict", + Usage: "Return non-zero exit when updaters report errors.", + }, + &cli.PathFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "clair configuration file", + Value: "config.yaml", + TakesFile: true, + }, + }, +} + +func exportAction(c *cli.Context) error { + ctx := c.Context + var out io.Writer + + // Setup the output file. + args := c.Args() + switch args.Len() { + case 0: + out = os.Stdout + case 1: + f, err := os.Create(args.First()) + if err != nil { + return err + } + defer f.Close() + out = f + default: + return errors.New("too many arguments (wanted at most one)") + } + + // Read and process the config file. + cfg, err := loadConfig(c.String("config")) + if err != nil { + return err + } + filter, err := regexp.Compile(cfg.Updaters.Filter) + if err != nil { + return err + } + cfgs := make(map[string]driver.ConfigUnmarshaler, len(cfg.Updaters.Config)) + for name, node := range cfg.Updaters.Config { + cfgs[name] = node.Decode + } + + cl, _, err := cfg.Client(nil) + if err != nil { + return err + } + + u, err := libvuln.NewOfflineUpdater(cfgs, filter.MatchString, out) + if err != nil { + return err + } + + defs := updater.Registered() + cfg.Updaters.FilterSets(defs) + if err := updater.Configure(ctx, defs, cfgs, cl); err != nil { + return err + } + ufs := make([]driver.UpdaterSetFactory, 0, len(defs)) + for _, u := range defs { + ufs = append(ufs, u) + } + + if err := u.RunUpdaters(ctx, ufs...); err != nil { + // Don't exit non-zero if we run into errors, unless the strict flag was + // provided. + code := 0 + if c.Bool("strict") { + code = 1 + } + return cli.Exit(err.Error(), code) + } + return nil +} diff --git a/cmd/clairctl/import.go b/cmd/clairctl/import.go new file mode 100644 index 0000000000..0e4ac7af41 --- /dev/null +++ b/cmd/clairctl/import.go @@ -0,0 +1,93 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + + "github.com/jackc/pgx/v4/pgxpool" + "github.com/quay/claircore/libvuln" + "github.com/urfave/cli/v2" +) + +// ImportCmd is the "import-updaters" subcommand. +var ImportCmd = &cli.Command{ + Name: "import-updaters", + Description: "Import updates from a file.", + Action: importAction, + Usage: "import updates", + ArgsUsage: "in", + Flags: []cli.Flag{ + &cli.PathFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "clair configuration file", + Value: "config.yaml", + TakesFile: true, + }, + }, +} + +func importAction(c *cli.Context) error { + ctx := c.Context + // Read and process the config file. + cfg, err := loadConfig(c.String("config")) + if err != nil { + return err + } + + cl, _, err := cfg.Client(nil) + if err != nil { + return err + } + + // Setup the input file. + args := c.Args() + if args.Len() != 1 { + return errors.New("need at least one argument") + } + inName := args.First() + + var in io.Reader + u, uerr := url.Parse(inName) + f, ferr := os.Open(inName) + if f != nil { + defer f.Close() + } + switch { + case uerr == nil: + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return err + } + res, err := cl.Do(req) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return err + } + if res.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected response: %d %s", res.StatusCode, res.Status) + } + in = res.Body + case ferr == nil: + in = f + default: + return fmt.Errorf("unable to make sense of argument %q", inName) + } + + pool, err := pgxpool.Connect(ctx, cfg.Matcher.ConnString) + if err != nil { + return err + } + defer pool.Close() + + if err := libvuln.OfflineImport(ctx, pool, in); err != nil { + return err + } + return nil +} diff --git a/cmd/clairctl/main.go b/cmd/clairctl/main.go index a56b019e8c..c91b4c5677 100644 --- a/cmd/clairctl/main.go +++ b/cmd/clairctl/main.go @@ -32,6 +32,8 @@ func main() { Commands: []*cli.Command{ ManifestCmd, ReportCmd, + ExportCmd, + ImportCmd, }, Flags: []cli.Flag{ &cli.BoolFlag{