From e0c135925b1e88961a006ee5352a90f4958a7fd8 Mon Sep 17 00:00:00 2001 From: Blake Rouse Date: Wed, 13 May 2020 12:16:52 -0400 Subject: [PATCH 1/3] Add a file lock to the data directory on startup to prevent multiple agents. --- .../pkg/agent/application/locker.go | 45 +++++++++++++++++++ .../pkg/agent/application/locker_test.go | 29 ++++++++++++ x-pack/elastic-agent/pkg/agent/cmd/run.go | 7 +++ .../pkg/artifact/download/fs/downloader.go | 2 +- 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 x-pack/elastic-agent/pkg/agent/application/locker.go create mode 100644 x-pack/elastic-agent/pkg/agent/application/locker_test.go diff --git a/x-pack/elastic-agent/pkg/agent/application/locker.go b/x-pack/elastic-agent/pkg/agent/application/locker.go new file mode 100644 index 00000000000..31195724e68 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/application/locker.go @@ -0,0 +1,45 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package application + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/gofrs/flock" +) + +const lockFileName = "agent.lock" + +var ErrAppAlreadyRunning = fmt.Errorf("another elastic-agent is already running") + +type AppLocker struct { + lock *flock.Flock +} + +func NewAppLocker(dir string) *AppLocker { + if _, err := os.Stat(dir); os.IsNotExist(err) { + _ = os.Mkdir(dir, 0755) + } + return &AppLocker{ + lock: flock.New(filepath.Join(dir, lockFileName)), + } +} + +func (a *AppLocker) TryLock() error { + locked, err := a.lock.TryLock() + if err != nil { + return err + } + if !locked { + return ErrAppAlreadyRunning + } + return nil +} + +func (a *AppLocker) Unlock() error { + return a.lock.Unlock() +} diff --git a/x-pack/elastic-agent/pkg/agent/application/locker_test.go b/x-pack/elastic-agent/pkg/agent/application/locker_test.go new file mode 100644 index 00000000000..5b8f4a8e812 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/application/locker_test.go @@ -0,0 +1,29 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package application + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAppLocker(t *testing.T) { + tmp, _ := ioutil.TempDir("", "locker") + defer os.RemoveAll(tmp) + + locker1 := NewAppLocker(tmp) + locker2 := NewAppLocker(tmp) + + require.NoError(t, locker1.TryLock()) + assert.Error(t, locker2.TryLock()) + require.NoError(t, locker1.Unlock()) + require.NoError(t, locker2.TryLock()) + assert.Error(t, locker1.TryLock()) + require.NoError(t, locker2.Unlock()) +} diff --git a/x-pack/elastic-agent/pkg/agent/cmd/run.go b/x-pack/elastic-agent/pkg/agent/cmd/run.go index db199e2b47d..cb0fd62923a 100644 --- a/x-pack/elastic-agent/pkg/agent/cmd/run.go +++ b/x-pack/elastic-agent/pkg/agent/cmd/run.go @@ -13,6 +13,7 @@ import ( "github.com/spf13/cobra" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/paths" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/cli" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" @@ -47,6 +48,12 @@ func run(flags *globalFlags, streams *cli.IOStreams) error { return err } + locker := application.NewAppLocker(paths.Data()) + if err := locker.TryLock(); err != nil { + return err + } + defer locker.Unlock() + app, err := application.New(logger, pathConfigFile) if err != nil { return err diff --git a/x-pack/elastic-agent/pkg/artifact/download/fs/downloader.go b/x-pack/elastic-agent/pkg/artifact/download/fs/downloader.go index 490f0b77478..cbbb0c2319a 100644 --- a/x-pack/elastic-agent/pkg/artifact/download/fs/downloader.go +++ b/x-pack/elastic-agent/pkg/artifact/download/fs/downloader.go @@ -42,7 +42,7 @@ func NewDownloader(config *artifact.Config) *Downloader { func (e *Downloader) Download(_ context.Context, programName, version string) (string, error) { // create a destination directory root/program destinationDir := filepath.Join(e.config.TargetDirectory, programName) - if err := os.MkdirAll(destinationDir, os.ModeDir); err != nil { + if err := os.MkdirAll(destinationDir, 0755); err != nil { return "", errors.New(err, "creating directory for downloaded artifact failed", errors.TypeFilesystem, errors.M(errors.MetaKeyPath, destinationDir)) } From 2422e0221ce169208f06e9447b968cf1564cf483 Mon Sep 17 00:00:00 2001 From: Blake Rouse Date: Wed, 13 May 2020 13:44:08 -0400 Subject: [PATCH 2/3] Add export comments to AppLocker. --- x-pack/elastic-agent/pkg/agent/application/locker.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/elastic-agent/pkg/agent/application/locker.go b/x-pack/elastic-agent/pkg/agent/application/locker.go index 31195724e68..fadd504f82d 100644 --- a/x-pack/elastic-agent/pkg/agent/application/locker.go +++ b/x-pack/elastic-agent/pkg/agent/application/locker.go @@ -14,12 +14,15 @@ import ( const lockFileName = "agent.lock" +// ErrAppAlreadyRunning error returned when another elastic-agent is already holding the lock. var ErrAppAlreadyRunning = fmt.Errorf("another elastic-agent is already running") +// AppLocker locks the agent.lock file inside the provided directory. type AppLocker struct { lock *flock.Flock } +// NewAppLocker creates an AppLocker that locks the agent.lock file inside the provided directory. func NewAppLocker(dir string) *AppLocker { if _, err := os.Stat(dir); os.IsNotExist(err) { _ = os.Mkdir(dir, 0755) @@ -29,6 +32,7 @@ func NewAppLocker(dir string) *AppLocker { } } +// TryLock tries to grab the lock file and returns error if it cannot. func (a *AppLocker) TryLock() error { locked, err := a.lock.TryLock() if err != nil { @@ -40,6 +44,7 @@ func (a *AppLocker) TryLock() error { return nil } +// Unlock releases the lock file. func (a *AppLocker) Unlock() error { return a.lock.Unlock() } From 0744577e89636b199a11264bfb585940b8da3e52 Mon Sep 17 00:00:00 2001 From: Blake Rouse Date: Thu, 14 May 2020 10:27:09 -0400 Subject: [PATCH 3/3] Fix periodic to not block startup. --- .../pkg/agent/application/periodic.go | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/x-pack/elastic-agent/pkg/agent/application/periodic.go b/x-pack/elastic-agent/pkg/agent/application/periodic.go index 1acf1a97377..ab4ac6592ef 100644 --- a/x-pack/elastic-agent/pkg/agent/application/periodic.go +++ b/x-pack/elastic-agent/pkg/agent/application/periodic.go @@ -23,21 +23,24 @@ type periodic struct { } func (p *periodic) Start() error { - if err := p.work(); err != nil { - p.log.Debugf("Failed to read configuration, error: %s", err) - } - - for { - select { - case <-p.done: - break - case <-time.After(p.period): - } - + go func() { if err := p.work(); err != nil { p.log.Debugf("Failed to read configuration, error: %s", err) } - } + + for { + select { + case <-p.done: + break + case <-time.After(p.period): + } + + if err := p.work(); err != nil { + p.log.Debugf("Failed to read configuration, error: %s", err) + } + } + }() + return nil } func (p *periodic) work() error {