Skip to content

Commit a84508c

Browse files
authored
[Elastic Agent] Add support for Fleet Server inside Docker (#24220)
* Add new container subcommand. * Fix vet. * Fix path with just enroll. * Add changelog. * Add FLEET_SETUP fallback. Make GET, POST to kibana for resilient. * Add FLEET_FORCE. Don't update Kibana config when Fleet Server running locally.
1 parent a529338 commit a84508c

13 files changed

Lines changed: 591 additions & 109 deletions

File tree

dev-tools/packaging/templates/docker/docker-entrypoint.elastic-agent.tmpl

Lines changed: 6 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,10 @@
22

33
set -eo pipefail
44

5-
# Environment variables used
6-
# FLEET_ENROLLMENT_TOKEN - existing enrollment token to be used for enroll
7-
# FLEET_ENROLL - if set to 1 enroll will be performed
8-
# FLEET_ENROLL_INSECURE - if set to 1, agent will enroll with fleet using --insecure flag
9-
# FLEET_SETUP - if set to 1 fleet setup will be performed
10-
# FLEET_TOKEN_NAME - token name for a token to be created
11-
# KIBANA_HOST - actual kibana host [http://localhost:5601]
12-
# KIBANA_PASSWORD - password for accessing kibana API [changeme]
13-
# KIBANA_USERNAME - username for accessing kibana API [elastic]
5+
# For information on the possible environment variables that can be passed into the container. Run the following
6+
# command for information on the options that are available.
7+
#
8+
# `./elastic-agent container --help`
9+
#
1410

15-
function setup(){
16-
curl -X POST ${KIBANA_HOST:-http://localhost:5601}/api/fleet/setup -H 'kbn-xsrf: true' -u ${KIBANA_USERNAME:-elastic}:${KIBANA_PASSWORD:-changeme}
17-
curl -X POST ${KIBANA_HOST:-http://localhost:5601}/api/fleet/agents/setup \
18-
-H 'Content-Type: application/json' \
19-
-H 'kbn-xsrf: true' \
20-
-u ${KIBANA_USERNAME:-elastic}:${KIBANA_PASSWORD:-changeme}
21-
}
22-
23-
function enroll(){
24-
local enrollResp
25-
local apiKey
26-
27-
if [[ -n "${FLEET_ENROLLMENT_TOKEN}" ]]; then
28-
apikey="${FLEET_ENROLLMENT_TOKEN}"
29-
else
30-
enrollResp=$(curl ${KIBANA_HOST:-http://localhost:5601}/api/fleet/enrollment-api-keys \
31-
-H 'Content-Type: application/json' \
32-
-H 'kbn-xsrf: true' \
33-
-u ${KIBANA_USERNAME:-elastic}:${KIBANA_PASSWORD:-changeme} )
34-
35-
local exitCode=$?
36-
if [ $exitCode -ne 0 ]; then
37-
exit $exitCode
38-
fi
39-
echo $enrollResp
40-
local apikeyId=$(echo $enrollResp | jq -r '.list[] | select((.name | startswith("Default ")) and (.active == true)) | .id')
41-
echo $apikeyId
42-
43-
if [[ -z "${apikeyId}" ]]; then
44-
echo "Default agent policy was not found. Please consider using own enrollment token (FLEET_ENROLLMENT_TOKEN)."
45-
exit 1
46-
fi
47-
48-
enrollResp=$(curl ${KIBANA_HOST:-http://localhost:5601}/api/fleet/enrollment-api-keys/$apikeyId \
49-
-H 'Content-Type: application/json' \
50-
-H 'kbn-xsrf: true' \
51-
-u ${KIBANA_USERNAME:-elastic}:${KIBANA_PASSWORD:-changeme} )
52-
53-
exitCode=$?
54-
if [ $exitCode -ne 0 ]; then
55-
exit $exitCode
56-
fi
57-
58-
apikey=$(echo $enrollResp | jq -r '.item.api_key')
59-
fi
60-
echo $apikey
61-
62-
if [[ -n "${FLEET_ENROLL_INSECURE}" ]] && [[ ${FLEET_ENROLL_INSECURE} == 1 ]]; then
63-
insecure_flag="--insecure"
64-
fi
65-
66-
./{{ .BeatName }} enroll ${insecure_flag} -f --url=${KIBANA_HOST:-http://localhost:5601} --enrollment-token=$apikey
67-
}
68-
69-
if [[ -n "${FLEET_SETUP}" ]] && [[ ${FLEET_SETUP} == 1 ]]; then setup; fi
70-
if [[ -n "${FLEET_ENROLL}" ]] && [[ ${FLEET_ENROLL} == 1 ]]; then enroll; fi
71-
72-
exec {{ .BeatName }} run "$@"
11+
exec {{ .BeatName }} container "$@"

x-pack/elastic-agent/CHANGELOG.next.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,4 @@
7373
- Add support for Fleet Server {pull}23736[23736]
7474
- Add support for enrollment with local bootstrap of Fleet Server {pull}23865[23865]
7575
- Add TLS support for Fleet Server {pull}24142[24142]
76+
- Add support for Fleet Server running under Elastic Agent {pull}24220[24220]

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

Lines changed: 96 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"os"
1616
"time"
1717

18+
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/process"
19+
1820
"gopkg.in/yaml.v2"
1921

2022
"github.com/elastic/beats/v7/libbeat/common/backoff"
@@ -72,6 +74,19 @@ type EnrollCmd struct {
7274
client clienter
7375
configStore store
7476
kibanaConfig *kibana.Config
77+
agentProc *process.Info
78+
}
79+
80+
// EnrollCmdFleetServerOption define all the supported enrollment options for bootstrapping with Fleet Server.
81+
type EnrollCmdFleetServerOption struct {
82+
ConnStr string
83+
PolicyID string
84+
Host string
85+
Port uint16
86+
Cert string
87+
CertKey string
88+
Insecure bool
89+
SpawnAgent bool
7590
}
7691

7792
// EnrollCmdOption define all the supported enrollment option.
@@ -84,13 +99,7 @@ type EnrollCmdOption struct {
8499
UserProvidedMetadata map[string]interface{}
85100
EnrollAPIKey string
86101
Staging string
87-
FleetServerConnStr string
88-
FleetServerPolicyID string
89-
FleetServerHost string
90-
FleetServerPort uint16
91-
FleetServerCert string
92-
FleetServerCertKey string
93-
FleetServerInsecure bool
102+
FleetServer EnrollCmdFleetServerOption
94103
}
95104

96105
func (e *EnrollCmdOption) kibanaConfig() (*kibana.Config, error) {
@@ -157,7 +166,8 @@ func NewEnrollCmdWithStore(
157166
// Execute tries to enroll the agent into Fleet.
158167
func (c *EnrollCmd) Execute(ctx context.Context) error {
159168
var err error
160-
if c.options.FleetServerConnStr != "" {
169+
defer c.stopAgent() // ensure its stopped no matter what
170+
if c.options.FleetServer.ConnStr != "" {
161171
err = c.fleetServerBootstrap(ctx)
162172
if err != nil {
163173
return err
@@ -185,18 +195,26 @@ func (c *EnrollCmd) Execute(ctx context.Context) error {
185195
return errors.New(err, "fail to enroll")
186196
}
187197

188-
if c.daemonReload(ctx) != nil {
189-
c.log.Info("Elastic Agent might not be running; unable to trigger restart")
198+
if c.agentProc == nil {
199+
if c.daemonReload(ctx) != nil {
200+
c.log.Info("Elastic Agent might not be running; unable to trigger restart")
201+
}
202+
c.log.Info("Successfully triggered restart on running Elastic Agent.")
203+
return nil
190204
}
191-
c.log.Info("Successfully triggered restart on running Elastic Agent.")
205+
c.log.Info("Elastic Agent has been enrolled; start Elastic Agent")
192206
return nil
193207
}
194208

195209
func (c *EnrollCmd) fleetServerBootstrap(ctx context.Context) error {
196210
c.log.Debug("verifying communication with running Elastic Agent daemon")
211+
agentRunning := true
197212
_, err := getDaemonStatus(ctx)
198213
if err != nil {
199-
return errors.New("failed to communicate with elastic-agent daemon; is elastic-agent running?")
214+
if !c.options.FleetServer.SpawnAgent {
215+
return errors.New("failed to communicate with elastic-agent daemon; is elastic-agent running?")
216+
}
217+
agentRunning = false
200218
}
201219

202220
err = c.prepareFleetTLS()
@@ -205,9 +223,9 @@ func (c *EnrollCmd) fleetServerBootstrap(ctx context.Context) error {
205223
}
206224

207225
fleetConfig, err := createFleetServerBootstrapConfig(
208-
c.options.FleetServerConnStr, c.options.FleetServerPolicyID,
209-
c.options.FleetServerHost, c.options.FleetServerPort,
210-
c.options.FleetServerCert, c.options.FleetServerCertKey)
226+
c.options.FleetServer.ConnStr, c.options.FleetServer.PolicyID,
227+
c.options.FleetServer.Host, c.options.FleetServer.Port,
228+
c.options.FleetServer.Cert, c.options.FleetServer.CertKey)
211229
configToStore := map[string]interface{}{
212230
"fleet": fleetConfig,
213231
}
@@ -219,9 +237,18 @@ func (c *EnrollCmd) fleetServerBootstrap(ctx context.Context) error {
219237
return errors.New(err, "could not save fleet server bootstrap information", errors.TypeFilesystem)
220238
}
221239

222-
err = c.daemonReload(ctx)
223-
if err != nil {
224-
return errors.New(err, "failed to trigger elastic-agent daemon reload", errors.TypeApplication)
240+
if agentRunning {
241+
// reload the already running agent
242+
err = c.daemonReload(ctx)
243+
if err != nil {
244+
return errors.New(err, "failed to trigger elastic-agent daemon reload", errors.TypeApplication)
245+
}
246+
} else {
247+
// spawn `run` as a subprocess so enroll can perform the bootstrap process of Fleet Server
248+
err = c.startAgent()
249+
if err != nil {
250+
return err
251+
}
225252
}
226253

227254
err = waitForFleetServer(ctx, c.log)
@@ -232,25 +259,25 @@ func (c *EnrollCmd) fleetServerBootstrap(ctx context.Context) error {
232259
}
233260

234261
func (c *EnrollCmd) prepareFleetTLS() error {
235-
host := c.options.FleetServerHost
262+
host := c.options.FleetServer.Host
236263
if host == "" {
237264
host = "localhost"
238265
}
239-
port := c.options.FleetServerPort
266+
port := c.options.FleetServer.Port
240267
if port == 0 {
241268
port = defaultFleetServerPort
242269
}
243-
if c.options.FleetServerCert != "" && c.options.FleetServerCertKey == "" {
270+
if c.options.FleetServer.Cert != "" && c.options.FleetServer.CertKey == "" {
244271
return errors.New("certificate private key is required when certificate provided")
245272
}
246-
if c.options.FleetServerCertKey != "" && c.options.FleetServerCert == "" {
273+
if c.options.FleetServer.CertKey != "" && c.options.FleetServer.Cert == "" {
247274
return errors.New("certificate is required when certificate private key is provided")
248275
}
249-
if c.options.FleetServerCert == "" && c.options.FleetServerCertKey == "" {
250-
if c.options.FleetServerInsecure {
276+
if c.options.FleetServer.Cert == "" && c.options.FleetServer.CertKey == "" {
277+
if c.options.FleetServer.Insecure {
251278
// running insecure, force the binding to localhost (unless specified)
252-
if c.options.FleetServerHost == "" {
253-
c.options.FleetServerHost = "localhost"
279+
if c.options.FleetServer.Host == "" {
280+
c.options.FleetServer.Host = "localhost"
254281
}
255282
c.options.URL = fmt.Sprintf("http://%s:%d", host, port)
256283
c.options.Insecure = true
@@ -270,8 +297,8 @@ func (c *EnrollCmd) prepareFleetTLS() error {
270297
if err != nil {
271298
return err
272299
}
273-
c.options.FleetServerCert = string(pair.Crt)
274-
c.options.FleetServerCertKey = string(pair.Key)
300+
c.options.FleetServer.Cert = string(pair.Crt)
301+
c.options.FleetServer.CertKey = string(pair.Key)
275302
c.options.URL = fmt.Sprintf("https://%s:%d", hostname, port)
276303
c.options.CAs = []string{string(ca.Crt())}
277304
}
@@ -295,8 +322,18 @@ func (c *EnrollCmd) enrollWithBackoff(ctx context.Context) error {
295322
signal := make(chan struct{})
296323
backExp := backoff.NewExpBackoff(signal, 60*time.Second, 10*time.Minute)
297324

298-
for errors.Is(err, fleetapi.ErrTooManyRequests) {
299-
c.log.Warn("Too many requests on the remote server, will retry in a moment.")
325+
for {
326+
retry := false
327+
if errors.Is(err, fleetapi.ErrTooManyRequests) {
328+
c.log.Warn("Too many requests on the remote server, will retry in a moment.")
329+
retry = true
330+
} else if errors.Is(err, fleetapi.ErrConnRefused) {
331+
c.log.Warn("Remote server is not ready to accept connections, will retry in a moment.")
332+
retry = true
333+
}
334+
if !retry {
335+
break
336+
}
300337
backExp.Wait()
301338
c.log.Info("Retrying to enroll...")
302339
err = c.enroll(ctx)
@@ -344,11 +381,11 @@ func (c *EnrollCmd) enroll(ctx context.Context) error {
344381
"sourceURI": staging,
345382
}
346383
}
347-
if c.options.FleetServerConnStr != "" {
384+
if c.options.FleetServer.ConnStr != "" {
348385
serverConfig, err := createFleetServerBootstrapConfig(
349-
c.options.FleetServerConnStr, c.options.FleetServerPolicyID,
350-
c.options.FleetServerHost, c.options.FleetServerPort,
351-
c.options.FleetServerCert, c.options.FleetServerCertKey)
386+
c.options.FleetServer.ConnStr, c.options.FleetServer.PolicyID,
387+
c.options.FleetServer.Host, c.options.FleetServer.Port,
388+
c.options.FleetServer.Cert, c.options.FleetServer.CertKey)
352389
if err != nil {
353390
return err
354391
}
@@ -390,6 +427,27 @@ func (c *EnrollCmd) enroll(ctx context.Context) error {
390427
return nil
391428
}
392429

430+
func (c *EnrollCmd) startAgent() error {
431+
cmd, err := os.Executable()
432+
if err != nil {
433+
return err
434+
}
435+
c.log.Info("Spawning Elastic Agent daemon as a subprocess to complete bootstrap process.")
436+
proc, err := process.Start(c.log, cmd, nil, os.Geteuid(), os.Getegid(), "run")
437+
if err != nil {
438+
return err
439+
}
440+
c.agentProc = proc
441+
return nil
442+
}
443+
444+
func (c *EnrollCmd) stopAgent() {
445+
if c.agentProc != nil {
446+
c.agentProc.StopWait()
447+
c.agentProc = nil
448+
}
449+
}
450+
393451
func yamlToReader(in interface{}) (io.Reader, error) {
394452
data, err := yaml.Marshal(in)
395453
if err != nil {
@@ -461,6 +519,10 @@ func waitForFleetServer(ctx context.Context, log *logger.Logger) error {
461519
// app has started and is running
462520
resChan <- waitResult{}
463521
break
522+
} else if app.Status == proto.Status_FAILED {
523+
// app completely failed; exit now
524+
resChan <- waitResult{err: errors.New(app.Message)}
525+
break
464526
}
465527
if app.Message != "" {
466528
appMsg := fmt.Sprintf("Fleet Server - %s", app.Message)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ func (h *handlerPolicyChange) Handle(ctx context.Context, a action, acker fleetA
6262
}
6363

6464
func (h *handlerPolicyChange) handleKibanaHosts(c *config.Config) (err error) {
65+
// do not update kibana host from policy; no setters provided with local Fleet Server
66+
if len(h.setters) == 0 {
67+
return nil
68+
}
69+
6570
cfg, err := configuration.NewFromConfig(c)
6671
if err != nil {
6772
return errors.New(err, "could not parse the configuration from the policy", errors.TypeConfig)

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ func newManaged(
181181
agentInfo: agentInfo,
182182
config: cfg,
183183
store: store,
184-
setters: []clientSetter{acker},
184+
}
185+
if cfg.Fleet.Server == nil {
186+
// setters only set when not running a local Fleet Server
187+
policyChanger.setters = []clientSetter{acker}
185188
}
186189
actionDispatcher.MustRegister(
187190
&fleetapi.ActionPolicyChange{},

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command {
7373
cmd.AddCommand(newEnrollCommandWithArgs(flags, args, streams))
7474
cmd.AddCommand(newInspectCommandWithArgs(flags, args, streams))
7575
cmd.AddCommand(newWatchCommandWithArgs(flags, args, streams))
76+
cmd.AddCommand(newContainerCommand(flags, args, streams))
7677

7778
// windows special hidden sub-command (only added on windows)
7879
reexec := newReExecWindowsCommand(flags, args, streams)

0 commit comments

Comments
 (0)