Skip to content

Commit 06cf975

Browse files
authored
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 e1a4741)
1 parent 2ae668a commit 06cf975

File tree

5 files changed

+102
-13
lines changed

5 files changed

+102
-13
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package application
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"path/filepath"
11+
12+
"github.com/gofrs/flock"
13+
)
14+
15+
const lockFileName = "agent.lock"
16+
17+
// ErrAppAlreadyRunning error returned when another elastic-agent is already holding the lock.
18+
var ErrAppAlreadyRunning = fmt.Errorf("another elastic-agent is already running")
19+
20+
// AppLocker locks the agent.lock file inside the provided directory.
21+
type AppLocker struct {
22+
lock *flock.Flock
23+
}
24+
25+
// NewAppLocker creates an AppLocker that locks the agent.lock file inside the provided directory.
26+
func NewAppLocker(dir string) *AppLocker {
27+
if _, err := os.Stat(dir); os.IsNotExist(err) {
28+
_ = os.Mkdir(dir, 0755)
29+
}
30+
return &AppLocker{
31+
lock: flock.New(filepath.Join(dir, lockFileName)),
32+
}
33+
}
34+
35+
// TryLock tries to grab the lock file and returns error if it cannot.
36+
func (a *AppLocker) TryLock() error {
37+
locked, err := a.lock.TryLock()
38+
if err != nil {
39+
return err
40+
}
41+
if !locked {
42+
return ErrAppAlreadyRunning
43+
}
44+
return nil
45+
}
46+
47+
// Unlock releases the lock file.
48+
func (a *AppLocker) Unlock() error {
49+
return a.lock.Unlock()
50+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package application
6+
7+
import (
8+
"io/ioutil"
9+
"os"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestAppLocker(t *testing.T) {
17+
tmp, _ := ioutil.TempDir("", "locker")
18+
defer os.RemoveAll(tmp)
19+
20+
locker1 := NewAppLocker(tmp)
21+
locker2 := NewAppLocker(tmp)
22+
23+
require.NoError(t, locker1.TryLock())
24+
assert.Error(t, locker2.TryLock())
25+
require.NoError(t, locker1.Unlock())
26+
require.NoError(t, locker2.TryLock())
27+
assert.Error(t, locker1.TryLock())
28+
require.NoError(t, locker2.Unlock())
29+
}

x-pack/elastic-agent/pkg/agent/application/periodic.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,24 @@ type periodic struct {
2323
}
2424

2525
func (p *periodic) Start() error {
26-
if err := p.work(); err != nil {
27-
p.log.Debugf("Failed to read configuration, error: %s", err)
28-
}
29-
30-
for {
31-
select {
32-
case <-p.done:
33-
break
34-
case <-time.After(p.period):
35-
}
36-
26+
go func() {
3727
if err := p.work(); err != nil {
3828
p.log.Debugf("Failed to read configuration, error: %s", err)
3929
}
40-
}
30+
31+
for {
32+
select {
33+
case <-p.done:
34+
break
35+
case <-time.After(p.period):
36+
}
37+
38+
if err := p.work(); err != nil {
39+
p.log.Debugf("Failed to read configuration, error: %s", err)
40+
}
41+
}
42+
}()
43+
return nil
4144
}
4245

4346
func (p *periodic) work() error {

x-pack/elastic-agent/pkg/agent/cmd/run.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/spf13/cobra"
1414

1515
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application"
16+
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/paths"
1617
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors"
1718
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/cli"
1819
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config"
@@ -47,6 +48,12 @@ func run(flags *globalFlags, streams *cli.IOStreams) error {
4748
return err
4849
}
4950

51+
locker := application.NewAppLocker(paths.Data())
52+
if err := locker.TryLock(); err != nil {
53+
return err
54+
}
55+
defer locker.Unlock()
56+
5057
app, err := application.New(logger, pathConfigFile)
5158
if err != nil {
5259
return err

x-pack/elastic-agent/pkg/artifact/download/fs/downloader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func NewDownloader(config *artifact.Config) *Downloader {
4242
func (e *Downloader) Download(_ context.Context, programName, version string) (string, error) {
4343
// create a destination directory root/program
4444
destinationDir := filepath.Join(e.config.TargetDirectory, programName)
45-
if err := os.MkdirAll(destinationDir, os.ModeDir); err != nil {
45+
if err := os.MkdirAll(destinationDir, 0755); err != nil {
4646
return "", errors.New(err, "creating directory for downloaded artifact failed", errors.TypeFilesystem, errors.M(errors.MetaKeyPath, destinationDir))
4747
}
4848

0 commit comments

Comments
 (0)