From 06cf975df73995e25d93e8d8b48f58b2424ecd6d Mon Sep 17 00:00:00 2001 From: Blake Rouse Date: Thu, 14 May 2020 12:59:52 -0400 Subject: [PATCH] Add a file lock to the data directory on startup to prevent multiple agents. (#18483) (#18529) * Add a file lock to the data directory on startup to prevent multiple agents. * Add export comments to AppLocker. * Fix periodic to not block startup. (cherry picked from commit e1a4741fd0bd67a0c7f32eabb3859b02b9d25c62) --- .../pkg/agent/application/locker.go | 50 +++++++++++++++++++ .../pkg/agent/application/locker_test.go | 29 +++++++++++ .../pkg/agent/application/periodic.go | 27 +++++----- x-pack/elastic-agent/pkg/agent/cmd/run.go | 7 +++ .../pkg/artifact/download/fs/downloader.go | 2 +- 5 files changed, 102 insertions(+), 13 deletions(-) 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..fadd504f82d --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/application/locker.go @@ -0,0 +1,50 @@ +// 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" + +// 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) + } + return &AppLocker{ + lock: flock.New(filepath.Join(dir, lockFileName)), + } +} + +// 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 { + return err + } + if !locked { + return ErrAppAlreadyRunning + } + return nil +} + +// Unlock releases the lock file. +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/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 { 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)) }