diff --git a/js/bundle.go b/js/bundle.go index 39bf259f6ff..accc4cd1796 100644 --- a/js/bundle.go +++ b/js/bundle.go @@ -334,6 +334,10 @@ func (b *Bundle) instantiate(logger logrus.FieldLogger, rt *goja.Runtime, init * unbindInit() *init.ctxPtr = nil + if vuID == 0 { + init.allowOnlyOpenedFiles() + } + rt.SetRandSource(common.NewRandSource()) return nil diff --git a/js/initcontext.go b/js/initcontext.go index 0847e3d1fa8..1371362a128 100644 --- a/js/initcontext.go +++ b/js/initcontext.go @@ -48,6 +48,7 @@ import ( "go.k6.io/k6/js/modules/k6/metrics" "go.k6.io/k6/js/modules/k6/ws" "go.k6.io/k6/lib" + "go.k6.io/k6/lib/fsext" "go.k6.io/k6/loader" ) @@ -299,13 +300,8 @@ func (i *InitContext) Open(ctx context.Context, filename string, args ...string) if filename[0:1] != afero.FilePathSeparator { filename = afero.FilePathSeparator + filename } - // Workaround for https://github.com/spf13/afero/issues/201 - if isDir, err := afero.IsDir(fs, filename); err != nil { - return nil, err - } else if isDir { - return nil, fmt.Errorf("open() can't be used with directories, path: %q", filename) - } - data, err := afero.ReadFile(fs, filename) + + data, err := readFile(fs, filename) if err != nil { return nil, err } @@ -317,6 +313,40 @@ func (i *InitContext) Open(ctx context.Context, filename string, args ...string) return i.runtime.ToValue(string(data)), nil } +func readFile(fileSystem afero.Fs, filename string) ([]byte, error) { + // Workaround for https://github.com/spf13/afero/issues/201 + if isDir, err := afero.IsDir(fileSystem, filename); err != nil { + return nil, err + } else if isDir { + return nil, fmt.Errorf("open() can't be used with directories, path: %q", filename) + } + + data, err := afero.ReadFile(fileSystem, filename) + if err == nil { + return data, nil + } + + // loading different files per VU is not supported, so all files should are going + // to be used inside the scenario should be opened during the init step (without any conditions) + if errors.Is(err, fsext.ErrFileNeverOpenedBefore) { + return nil, fmt.Errorf("open() can't be used under the conditions, path: %q", filename) + } + + return nil, err +} + +// allowOnlyOpenedFiles enables seen only files +func (i *InitContext) allowOnlyOpenedFiles() { + fs := i.filesystems["file"] + + alreadyOpenedFS, ok := fs.(fsext.OnlyOpenedEnabler) + if !ok { + return + } + + alreadyOpenedFS.AllowOnlyOpened() +} + func getInternalJSModules() map[string]interface{} { return map[string]interface{}{ "k6": k6.New(), diff --git a/js/runner_test.go b/js/runner_test.go index 34abe6a8b60..8a6f54de632 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -55,6 +55,7 @@ import ( "go.k6.io/k6/js/modules/k6/ws" "go.k6.io/k6/lib" _ "go.k6.io/k6/lib/executor" // TODO: figure out something better + "go.k6.io/k6/lib/fsext" "go.k6.io/k6/lib/metrics" "go.k6.io/k6/lib/testutils" "go.k6.io/k6/lib/testutils/httpmultibin" @@ -1157,6 +1158,31 @@ func TestVUIntegrationOpenFunctionErrorWhenSneaky(t *testing.T) { assert.Contains(t, err.Error(), "only available in the init stage") } +func TestVUDoesNotOpenUnderConditions(t *testing.T) { + t.Parallel() + + baseFS := afero.NewMemMapFs() + data := ` + if (__VU > 0) { + data = open("/home/somebody/test.json"); + } + exports.default = function(data) { + console.log("hey") + } + ` + require.NoError(t, afero.WriteFile(baseFS, "/home/somebody/test.json", []byte(`42`), os.ModePerm)) + require.NoError(t, afero.WriteFile(baseFS, "/script.js", []byte(data), os.ModePerm)) + + fs := fsext.NewCacheOnReadFs(baseFS, afero.NewMemMapFs(), 0) + + r, err := getSimpleRunner(t, "/script.js", data, fs) + require.NoError(t, err) + + _, err = r.NewVU(1, 1, make(chan stats.SampleContainer, 100)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "open() can't be used under the conditions") +} + func TestVUIntegrationCookiesReset(t *testing.T) { t.Parallel() tb := httpmultibin.NewHTTPMultiBin(t) diff --git a/lib/archive.go b/lib/archive.go index 35d3937dbd9..0116344c9fe 100644 --- a/lib/archive.go +++ b/lib/archive.go @@ -251,7 +251,7 @@ func (arc *Archive) Write(out io.Writer) error { normalizeAndAnonymizeURL(metaArc.PwdURL) metaArc.Filename = getURLtoString(metaArc.FilenameURL) metaArc.Pwd = getURLtoString(metaArc.PwdURL) - var actualDataPath, err = url.PathUnescape(path.Join(getURLPathOnFs(metaArc.FilenameURL))) + actualDataPath, err := url.PathUnescape(path.Join(getURLPathOnFs(metaArc.FilenameURL))) if err != nil { return err } @@ -286,7 +286,7 @@ func (arc *Archive) Write(out io.Writer) error { if !ok { continue } - if cachedfs, ok := filesystem.(fsext.CacheOnReadFs); ok { + if cachedfs, ok := filesystem.(fsext.CacheLayerGetter); ok { filesystem = cachedfs.GetCachingFs() } @@ -344,7 +344,7 @@ func (arc *Archive) Write(out io.Writer) error { } for _, filePath := range paths { - var fullFilePath = path.Clean(path.Join(name, filePath)) + fullFilePath := path.Clean(path.Join(name, filePath)) // we either have opaque if fullFilePath == actualDataPath { madeLinkToData = true diff --git a/lib/fsext/cacheonread.go b/lib/fsext/cacheonread.go index 7f57ddcdc22..4ee59e38cb9 100644 --- a/lib/fsext/cacheonread.go +++ b/lib/fsext/cacheonread.go @@ -21,27 +21,77 @@ package fsext import ( + "errors" + "sync" "time" "github.com/spf13/afero" ) +// ErrFileNeverOpenedBefore represent an error when file never opened before +var ErrFileNeverOpenedBefore = errors.New("file wasn't opened before") + // CacheOnReadFs is wrapper around afero.CacheOnReadFs with the ability to return the filesystem // that is used as cache type CacheOnReadFs struct { afero.Fs cache afero.Fs + + lock *sync.Mutex + openedOnly bool + opened map[string]struct{} +} + +// OnlyOpenedEnabler enables the mode of FS that allows to open +// already opened files (e.g. serve from cache only) +type OnlyOpenedEnabler interface { + AllowOnlyOpened() +} + +// CacheLayerGetter provide a direct access to a cache layer +type CacheLayerGetter interface { + GetCachingFs() afero.Fs } // NewCacheOnReadFs returns a new CacheOnReadFs func NewCacheOnReadFs(base, layer afero.Fs, cacheTime time.Duration) afero.Fs { - return CacheOnReadFs{ + return &CacheOnReadFs{ Fs: afero.NewCacheOnReadFs(base, layer, cacheTime), cache: layer, + + lock: &sync.Mutex{}, + openedOnly: false, + opened: map[string]struct{}{}, } } // GetCachingFs returns the afero.Fs being used for cache -func (c CacheOnReadFs) GetCachingFs() afero.Fs { +func (c *CacheOnReadFs) GetCachingFs() afero.Fs { // nolint:ireturn return c.cache } + +// AllowOnlyOpened enables the opened only mode of the CacheOnReadFs +func (c *CacheOnReadFs) AllowOnlyOpened() { + c.lock.Lock() + defer c.lock.Unlock() + + c.openedOnly = true +} + +// Open opens file and track the history of opened files +// if CacheOnReadFs is in the opened only mode it should return +// an error if file wasn't open before +func (c *CacheOnReadFs) Open(name string) (afero.File, error) { // nolint:ireturn + c.lock.Lock() + defer c.lock.Unlock() + + if !c.openedOnly { + c.opened[name] = struct{}{} + } else { + if _, ok := c.opened[name]; !ok { + return nil, ErrFileNeverOpenedBefore + } + } + + return c.Fs.Open(name) +} diff --git a/loader/readsource.go b/loader/readsource.go index 5211f8abd91..67079176427 100644 --- a/loader/readsource.go +++ b/loader/readsource.go @@ -44,7 +44,7 @@ func ReadSource( return nil, err } // TODO: don't do it in this way ... - err = afero.WriteFile(filesystems["file"].(fsext.CacheOnReadFs).GetCachingFs(), "/-", data, 0644) + err = afero.WriteFile(filesystems["file"].(fsext.CacheLayerGetter).GetCachingFs(), "/-", data, 0o644) if err != nil { return nil, fmt.Errorf("caching data read from -: %w", err) }