From 796d14415b3015a65718c26172eef2dbdb5aaf9e Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 2 Dec 2024 17:05:34 -0500 Subject: [PATCH] Fix DB v6 curator directory creation (#2293) * fix DB dir creation Signed-off-by: Alex Goodman * add dne test for v6 client Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- grype/db/v6/distribution/client.go | 6 +- grype/db/v6/distribution/client_test.go | 13 +++ grype/db/v6/installation/curator.go | 20 ++++- grype/db/v6/installation/curator_test.go | 109 +++++++++++++++++++++++ 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/grype/db/v6/distribution/client.go b/grype/db/v6/distribution/client.go index 87c7960d957..1fdd590243d 100644 --- a/grype/db/v6/distribution/client.go +++ b/grype/db/v6/distribution/client.go @@ -128,10 +128,14 @@ func (c client) isUpdateAvailable(current *v6.Description, candidate *LatestDocu func (c client) Download(archive Archive, dest string, downloadProgress *progress.Manual) (string, error) { defer downloadProgress.SetCompleted() + if err := os.MkdirAll(dest, 0700); err != nil { + return "", fmt.Errorf("unable to create db download root dir: %w", err) + } + // note: as much as I'd like to use the afero FS abstraction here, the go-getter library does not support it tempDir, err := os.MkdirTemp(dest, "grype-db-download") if err != nil { - return "", fmt.Errorf("unable to create db temp dir: %w", err) + return "", fmt.Errorf("unable to create db client temp dir: %w", err) } // download the db to the temp dir diff --git a/grype/db/v6/distribution/client_test.go b/grype/db/v6/distribution/client_test.go index 7240a911e90..b9ac8841593 100644 --- a/grype/db/v6/distribution/client_test.go +++ b/grype/db/v6/distribution/client_test.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "net/http/httptest" + "path/filepath" "testing" "time" @@ -172,6 +173,18 @@ func TestClient_Download(t *testing.T) { mg.AssertExpectations(t) }) + + t.Run("nested into dir that does not exist", func(t *testing.T) { + c, mg := setup() + mg.On("GetToDir", mock.Anything, "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123", mock.Anything).Return(nil) + + nestedPath := filepath.Join(destDir, "nested") + tempDir, err := c.Download(*archive, nestedPath, &progress.Manual{}) + require.NoError(t, err) + require.True(t, len(tempDir) > 0) + + mg.AssertExpectations(t) + }) } func TestClient_IsUpdateAvailable(t *testing.T) { diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go index e76de04032c..98f75f33149 100644 --- a/grype/db/v6/installation/curator.go +++ b/grype/db/v6/installation/curator.go @@ -287,10 +287,14 @@ func (c curator) Import(dbArchivePath string) error { mon.Set("unarchiving") defer mon.SetCompleted() + if err := os.MkdirAll(c.config.DBRootDir, 0700); err != nil { + return fmt.Errorf("unable to create db root dir: %w", err) + } + // note: the temp directory is persisted upon download/validation/activation failure to allow for investigation tempDir, err := os.MkdirTemp(c.config.DBRootDir, fmt.Sprintf("tmp-v%v-import", db.ModelVersion)) if err != nil { - return fmt.Errorf("unable to create db temp dir: %w", err) + return fmt.Errorf("unable to create db import temp dir: %w", err) } err = archiver.Unarchive(dbArchivePath, tempDir) @@ -336,8 +340,13 @@ func (c curator) activate(dbDirPath string, mon monitor) error { mon.Set("activating") + return c.replaceDB(dbDirPath) +} + +// replaceDB swaps over to using the given path. +func (c curator) replaceDB(dbDirPath string) error { dbDir := c.config.DBDirectoryPath() - _, err = c.fs.Stat(dbDir) + _, err := c.fs.Stat(dbDir) if !os.IsNotExist(err) { // remove any previous databases err = c.Delete() @@ -346,8 +355,13 @@ func (c curator) activate(dbDirPath string, mon monitor) error { } } + // ensure parent db directory exists + if err := c.fs.MkdirAll(filepath.Dir(dbDir), 0700); err != nil { + return fmt.Errorf("unable to create db parent directory: %w", err) + } + // activate the new db cache by moving the temp dir to final location - return os.Rename(dbDirPath, dbDir) + return c.fs.Rename(dbDirPath, dbDir) } func (c curator) validateIntegrity(metadata *db.Description, dbFilePath string, validateChecksum bool) (*db.Description, string, error) { diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go index f2a656b0e62..2b64d0fdb5c 100644 --- a/grype/db/v6/installation/curator_test.go +++ b/grype/db/v6/installation/curator_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/spf13/afero" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/wagoodman/go-progress" @@ -547,6 +548,114 @@ func TestCurator_ValidateIntegrity(t *testing.T) { }) } +func TestReplaceDB(t *testing.T) { + cases := []struct { + name string + config Config + expected map[string]string // expected file name to content mapping in the DB dir + init func(t *testing.T, dir string, dbDir string) afero.Fs + wantErr require.ErrorAssertionFunc + verify func(t *testing.T, fs afero.Fs, config Config, expected map[string]string) + }{ + { + name: "replace non-existent DB", + config: Config{ + DBRootDir: "/test", + }, + expected: map[string]string{ + "file.txt": "new content", + }, + init: func(t *testing.T, dir string, dbDir string) afero.Fs { + fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) + require.NoError(t, fs.MkdirAll(dir, 0700)) + require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("new content"), 0644)) + return fs + }, + }, + { + name: "replace existing DB", + config: Config{ + DBRootDir: "/test", + }, + expected: map[string]string{ + "new_file.txt": "new content", + }, + init: func(t *testing.T, dir string, dbDir string) afero.Fs { + fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) + require.NoError(t, fs.MkdirAll(dbDir, 0700)) + require.NoError(t, afero.WriteFile(fs, filepath.Join(dbDir, "old_file.txt"), []byte("old content"), 0644)) + require.NoError(t, fs.MkdirAll(dir, 0700)) + require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "new_file.txt"), []byte("new content"), 0644)) + return fs + }, + }, + { + name: "non-existent parent dir creation", + config: Config{ + DBRootDir: "/dir/does/not/exist/db3", + }, + expected: map[string]string{ + "file.txt": "new content", + }, + init: func(t *testing.T, dir string, dbDir string) afero.Fs { + fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) + require.NoError(t, fs.MkdirAll(dir, 0700)) + require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("new content"), 0644)) + return fs + }, + }, + { + name: "error during rename", + config: Config{ + DBRootDir: "/test", + }, + expected: nil, // no files expected since operation fails + init: func(t *testing.T, dir string, dbDir string) afero.Fs { + fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) + require.NoError(t, fs.MkdirAll(dir, 0700)) + require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("content"), 0644)) + return afero.NewReadOnlyFs(fs) + }, + wantErr: require.Error, + verify: func(t *testing.T, fs afero.Fs, config Config, expected map[string]string) { + _, err := fs.Stat(config.DBDirectoryPath()) + require.Error(t, err) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.wantErr == nil { + tc.wantErr = require.NoError + } + dbDir := tc.config.DBDirectoryPath() + candidateDir := "/temp/db" + fs := tc.init(t, candidateDir, dbDir) + + c := curator{ + fs: fs, + config: tc.config, + } + + err := c.replaceDB(candidateDir) + tc.wantErr(t, err) + if tc.verify != nil { + tc.verify(t, fs, tc.config, tc.expected) + } + if err != nil { + return + } + for fileName, expectedContent := range tc.expected { + filePath := filepath.Join(tc.config.DBDirectoryPath(), fileName) + actualContent, err := afero.ReadFile(fs, filePath) + assert.NoError(t, err) + assert.Equal(t, expectedContent, string(actualContent)) + } + }) + } +} + func setupTestDB(t *testing.T, dbDir string) db.ReadWriter { s, err := db.NewWriter(db.Config{ DBDirPath: dbDir,