From b68ef7ed73c91557a2a44afb5b90d3941eaa13b4 Mon Sep 17 00:00:00 2001 From: Blake Rouse Date: Mon, 29 Jun 2020 08:51:43 -0400 Subject: [PATCH] [Elastic Agent] Support the install, control, and uninstall of Endpoint (#19248) * Initial spec parsing for endpoint. * Update comment. * Fix spec test. * Update code so it copies the entire input. * Fix ast test. * Merge agent-improve-restart-loop * Merge agent-endpoint-spec * Refactor core/plugin/app into mostly core/ and use core/plugin for different app types. * Work on endpoint service application. * More fixes. * Fix format and tests. * Fix some imports. * More cleanups. * Fix export comment. * Pass the program.Spec into the descriptor. * Run endpoint verify, install, and uninstall when endpoint should be running. * Fix install and uninstall of Endpoint * Fix some small issues with service app. * Add changelog entry. * Fix lint and tests. * Fix lint. * Remove the code no longer needed because of newer config format. * Fix rules and review. * Update to Endpoint Security. * Fix issues so endpoint security runs. * Add comments. * Update docstring. * Some more fixes. * Delete the extra endpoint testdata files. * Add timeout to exec_file step. * Fix supported map. * Fix getting support programs by cmd. * Improve app started checks. * Fix buildspec. --- x-pack/elastic-agent/CHANGELOG.asciidoc | 1 + .../dev-tools/cmd/buildspec/buildspec.go | 6 +- .../pkg/agent/application/config.go | 4 +- .../pkg/agent/application/fleet_decorator.go | 8 +-- .../pkg/agent/application/info/agent_id.go | 4 +- .../pkg/agent/application/stream.go | 7 ++ .../pkg/agent/operation/common_test.go | 27 ++++++-- .../pkg/agent/operation/monitoring_test.go | 8 ++- .../pkg/agent/operation/operation.go | 3 +- .../pkg/agent/operation/operation_config.go | 2 +- .../pkg/agent/operation/operation_fetch.go | 2 +- .../pkg/agent/operation/operation_install.go | 18 ++--- .../pkg/agent/operation/operation_remove.go | 2 +- .../agent/operation/operation_retryable.go | 6 +- .../pkg/agent/operation/operation_start.go | 8 +-- .../pkg/agent/operation/operation_stop.go | 2 +- .../agent/operation/operation_uninstall.go | 55 +++++++++++++++ .../pkg/agent/operation/operation_verify.go | 4 +- .../pkg/agent/operation/operator.go | 18 +++-- .../pkg/agent/program/program_test.go | 2 +- .../elastic-agent/pkg/agent/program/spec.go | 16 +++-- .../pkg/agent/program/spec_test.go | 20 ++++++ .../pkg/agent/program/supported.go | 8 +-- ...l => endpoint_basic-endpoint-security.yml} | 0 ...ml => single_config-endpoint-security.yml} | 0 .../pkg/agent/transpiler/rules.go | 3 +- .../pkg/agent/transpiler/steps.go | 68 +++++++++++++++++-- .../pkg/artifact/install/dir/dir_checker.go | 24 +++++++ .../artifact/install/hooks/hooks_installer.go | 63 ++++++++++------- .../pkg/artifact/install/installer.go | 17 ++++- .../pkg/artifact/install/tar/tar_installer.go | 11 ++- .../pkg/artifact/install/zip/zip_installer.go | 18 ++--- .../uninstall/hooks/hooks_uninstaller.go | 41 +++++++++++ .../pkg/artifact/uninstall/uninstaller.go | 28 ++++++++ .../pkg/core/plugin/process/app.go | 5 ++ .../pkg/core/plugin/process/status.go | 2 +- .../pkg/core/plugin/service/app.go | 5 ++ x-pack/elastic-agent/spec/endpoint.yml | 25 ++++++- 38 files changed, 435 insertions(+), 106 deletions(-) create mode 100644 x-pack/elastic-agent/pkg/agent/operation/operation_uninstall.go rename x-pack/elastic-agent/pkg/agent/program/testdata/{endpoint_basic-endpoint.yml => endpoint_basic-endpoint-security.yml} (100%) rename x-pack/elastic-agent/pkg/agent/program/testdata/{single_config-endpoint.yml => single_config-endpoint-security.yml} (100%) create mode 100644 x-pack/elastic-agent/pkg/artifact/install/dir/dir_checker.go create mode 100644 x-pack/elastic-agent/pkg/artifact/uninstall/hooks/hooks_uninstaller.go create mode 100644 x-pack/elastic-agent/pkg/artifact/uninstall/uninstaller.go diff --git a/x-pack/elastic-agent/CHANGELOG.asciidoc b/x-pack/elastic-agent/CHANGELOG.asciidoc index 8f947745f1b..c66798a01da 100644 --- a/x-pack/elastic-agent/CHANGELOG.asciidoc +++ b/x-pack/elastic-agent/CHANGELOG.asciidoc @@ -72,3 +72,4 @@ - Change stream.* to dataset.* fields {pull}18967[18967] - Agent now runs the GRPC server and spawned application connect by to Agent {pull}18973[18973] - Rename input.type logs to logfile {pull}19360[19360] +- Agent now installs/uninstalls Elastic Endpoint {pull}19248[19248] diff --git a/x-pack/elastic-agent/dev-tools/cmd/buildspec/buildspec.go b/x-pack/elastic-agent/dev-tools/cmd/buildspec/buildspec.go index 980c3d90d33..59ccdfa88e4 100644 --- a/x-pack/elastic-agent/dev-tools/cmd/buildspec/buildspec.go +++ b/x-pack/elastic-agent/dev-tools/cmd/buildspec/buildspec.go @@ -42,7 +42,7 @@ import ( ) var Supported []Spec -var SupportedMap map[string]bool +var SupportedMap map[string]Spec func init() { // Packed Files @@ -50,7 +50,7 @@ func init() { // {{ $f }} {{ end -}} unpacked := packer.MustUnpack("{{ .Pack }}") - SupportedMap = make(map[string]bool) + SupportedMap = make(map[string]Spec) for f, v := range unpacked { s, err:= NewSpecFromBytes(v) @@ -58,7 +58,7 @@ func init() { panic("Cannot read spec from " + f) } Supported = append(Supported, s) - SupportedMap[strings.ToLower(s.Name)] = true + SupportedMap[strings.ToLower(s.Cmd)] = s } } `)) diff --git a/x-pack/elastic-agent/pkg/agent/application/config.go b/x-pack/elastic-agent/pkg/agent/application/config.go index bca9e615e17..b76f6afaf21 100644 --- a/x-pack/elastic-agent/pkg/agent/application/config.go +++ b/x-pack/elastic-agent/pkg/agent/application/config.go @@ -108,12 +108,12 @@ func localConfigDefault() *localConfig { type FleetAgentConfig struct { API *APIAccess `config:"api" yaml:"api"` Reporting *LogReporting `config:"reporting" yaml:"reporting"` - Info *AgentInfo `config:"agent_info" yaml:"agent_info"` + Info *AgentInfo `config:"agent" yaml:"agent"` } // AgentInfo is a set of agent information. type AgentInfo struct { - ID string `json:"ID" yaml:"ID" config:"ID"` + ID string `json:"id" yaml:"id" config:"id"` } // APIAccess contains the required details to connect to the Kibana endpoint. diff --git a/x-pack/elastic-agent/pkg/agent/application/fleet_decorator.go b/x-pack/elastic-agent/pkg/agent/application/fleet_decorator.go index c4d963506e4..22a3ab0e966 100644 --- a/x-pack/elastic-agent/pkg/agent/application/fleet_decorator.go +++ b/x-pack/elastic-agent/pkg/agent/application/fleet_decorator.go @@ -24,13 +24,13 @@ func injectFleet(cfg *config.Config) func(*logger.Logger, *transpiler.AST) error } api, ok := transpiler.Lookup(ast, "api") if !ok { - return fmt.Errorf("failed to get api from fleet config") + return fmt.Errorf("failed to get api key from fleet config") } - agentInfo, ok := transpiler.Lookup(ast, "agent_info") + agent, ok := transpiler.Lookup(ast, "agent") if !ok { - return fmt.Errorf("failed to get agent_info from fleet config") + return fmt.Errorf("failed to get agent key from fleet config") } - fleet := transpiler.NewDict([]transpiler.Node{agentInfo, api}) + fleet := transpiler.NewDict([]transpiler.Node{agent, api}) err = transpiler.Insert(rootAst, fleet, "fleet") if err != nil { return err diff --git a/x-pack/elastic-agent/pkg/agent/application/info/agent_id.go b/x-pack/elastic-agent/pkg/agent/application/info/agent_id.go index 9e0084bc6f1..e0b3bf3e840 100644 --- a/x-pack/elastic-agent/pkg/agent/application/info/agent_id.go +++ b/x-pack/elastic-agent/pkg/agent/application/info/agent_id.go @@ -21,13 +21,13 @@ import ( // defaultAgentConfigFile is a name of file used to store agent information const defaultAgentConfigFile = "fleet.yml" -const agentInfoKey = "agent_info" +const agentInfoKey = "agent" // defaultAgentActionStoreFile is the file that will contains the action that can be replayed after restart. const defaultAgentActionStoreFile = "action_store.yml" type persistentAgentInfo struct { - ID string `json:"ID" yaml:"ID" config:"ID"` + ID string `json:"id" yaml:"id" config:"id"` } type ioStore interface { diff --git a/x-pack/elastic-agent/pkg/agent/application/stream.go b/x-pack/elastic-agent/pkg/agent/application/stream.go index 1f35b85585b..88262300d9a 100644 --- a/x-pack/elastic-agent/pkg/agent/application/stream.go +++ b/x-pack/elastic-agent/pkg/agent/application/stream.go @@ -13,6 +13,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/stateresolver" downloader "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/download/localremote" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/install" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/uninstall" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/monitoring" @@ -66,6 +67,11 @@ func newOperator(ctx context.Context, log *logger.Logger, id routingKey, config return nil, errors.New(err, "initiating installer") } + uninstaller, err := uninstall.NewUninstaller() + if err != nil { + return nil, errors.New(err, "initiating uninstaller") + } + stateResolver, err := stateresolver.NewStateResolver(log) if err != nil { return nil, err @@ -79,6 +85,7 @@ func newOperator(ctx context.Context, log *logger.Logger, id routingKey, config fetcher, verifier, installer, + uninstaller, stateResolver, srv, r, diff --git a/x-pack/elastic-agent/pkg/agent/operation/common_test.go b/x-pack/elastic-agent/pkg/agent/operation/common_test.go index 8b7a27c77ef..c314a5d7fdb 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/common_test.go +++ b/x-pack/elastic-agent/pkg/agent/operation/common_test.go @@ -12,13 +12,13 @@ import ( "testing" "time" - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/program" - operatorCfg "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/operation/config" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/program" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/stateresolver" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/download" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/install" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/uninstall" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/app" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" @@ -55,7 +55,8 @@ func getTestOperator(t *testing.T, downloadPath string, installPath string, p *a fetcher := &DummyDownloader{} verifier := &DummyVerifier{} - installer := &DummyInstaller{} + installer := &DummyInstallerChecker{} + uninstaller := &DummyUninstaller{} stateResolver, err := stateresolver.NewStateResolver(l) if err != nil { @@ -70,7 +71,7 @@ func getTestOperator(t *testing.T, downloadPath string, installPath string, p *a t.Fatal(err) } - operator, err := NewOperator(context.Background(), l, "p1", cfg, fetcher, verifier, installer, stateResolver, srv, nil, noop.NewMonitor()) + operator, err := NewOperator(context.Background(), l, "p1", cfg, fetcher, verifier, installer, uninstaller, stateResolver, srv, nil, noop.NewMonitor()) if err != nil { t.Fatal(err) } @@ -157,10 +158,22 @@ func (*DummyVerifier) Verify(p, v string) (bool, error) { var _ download.Verifier = &DummyVerifier{} -type DummyInstaller struct{} +type DummyInstallerChecker struct{} + +func (*DummyInstallerChecker) Check(_ context.Context, p, v, _ string) error { + return nil +} + +func (*DummyInstallerChecker) Install(_ context.Context, p, v, _ string) error { + return nil +} + +var _ install.InstallerChecker = &DummyInstallerChecker{} + +type DummyUninstaller struct{} -func (*DummyInstaller) Install(p, v, _ string) error { +func (*DummyUninstaller) Uninstall(_ context.Context, p, v, _ string) error { return nil } -var _ install.Installer = &DummyInstaller{} +var _ uninstall.Uninstaller = &DummyUninstaller{} diff --git a/x-pack/elastic-agent/pkg/agent/operation/monitoring_test.go b/x-pack/elastic-agent/pkg/agent/operation/monitoring_test.go index 2cdba2f37c2..e53e16b08e5 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/monitoring_test.go +++ b/x-pack/elastic-agent/pkg/agent/operation/monitoring_test.go @@ -120,7 +120,8 @@ func getMonitorableTestOperator(t *testing.T, installPath string, m monitoring.M fetcher := &DummyDownloader{} verifier := &DummyVerifier{} - installer := &DummyInstaller{} + installer := &DummyInstallerChecker{} + uninstaller := &DummyUninstaller{} stateResolver, err := stateresolver.NewStateResolver(l) if err != nil { @@ -132,7 +133,7 @@ func getMonitorableTestOperator(t *testing.T, installPath string, m monitoring.M } ctx := context.Background() - operator, err := NewOperator(ctx, l, "p1", cfg, fetcher, verifier, installer, stateResolver, srv, nil, m) + operator, err := NewOperator(ctx, l, "p1", cfg, fetcher, verifier, installer, uninstaller, stateResolver, srv, nil, m) if err != nil { t.Fatal(err) } @@ -146,7 +147,8 @@ type testMonitorableApp struct { monitor monitoring.Monitor } -func (*testMonitorableApp) Name() string { return "" } +func (*testMonitorableApp) Name() string { return "" } +func (*testMonitorableApp) Started() bool { return false } func (*testMonitorableApp) Start(_ context.Context, _ app.Taggable, cfg map[string]interface{}) error { return nil } diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation.go b/x-pack/elastic-agent/pkg/agent/operation/operation.go index 8e62d5961a7..3f5201f2fed 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation.go @@ -27,7 +27,7 @@ type operation interface { // examples: // - Start does not need to run if process is running // - Fetch does not need to run if package is already present - Check(application Application) (bool, error) + Check(ctx context.Context, application Application) (bool, error) // Run runs the operation Run(ctx context.Context, application Application) error } @@ -35,6 +35,7 @@ type operation interface { // Application is an application capable of being started, stopped and configured. type Application interface { Name() string + Started() bool Start(ctx context.Context, p app.Taggable, cfg map[string]interface{}) error Stop() Configure(ctx context.Context, config map[string]interface{}) error diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_config.go b/x-pack/elastic-agent/pkg/agent/operation/operation_config.go index d402b95dc53..ca52b8791ae 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_config.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_config.go @@ -47,7 +47,7 @@ func (o *operationConfig) Name() string { // Check checks whether config needs to be run. // // Always returns true. -func (o *operationConfig) Check(_ Application) (bool, error) { return true, nil } +func (o *operationConfig) Check(_ context.Context, _ Application) (bool, error) { return true, nil } // Run runs the operation func (o *operationConfig) Run(ctx context.Context, application Application) (err error) { diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_fetch.go b/x-pack/elastic-agent/pkg/agent/operation/operation_fetch.go index 694cffd6dc4..bb01890347b 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_fetch.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_fetch.go @@ -46,7 +46,7 @@ func (o *operationFetch) Name() string { // Check checks whether fetch needs to occur. // // If the artifacts already exists then fetch will not be ran. -func (o *operationFetch) Check(_ Application) (bool, error) { +func (o *operationFetch) Check(_ context.Context, _ Application) (bool, error) { downloadConfig := o.operatorConfig.DownloadConfig fullPath, err := artifact.GetArtifactPath(o.program.BinaryName(), o.program.Version(), downloadConfig.OS(), downloadConfig.Arch(), downloadConfig.TargetDirectory) if err != nil { diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_install.go b/x-pack/elastic-agent/pkg/agent/operation/operation_install.go index ad30668667d..883b895d4d2 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_install.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_install.go @@ -6,7 +6,6 @@ package operation import ( "context" - "os" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/operation/config" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/install" @@ -20,14 +19,14 @@ type operationInstall struct { logger *logger.Logger program Descriptor operatorConfig *config.Config - installer install.Installer + installer install.InstallerChecker } func newOperationInstall( logger *logger.Logger, program Descriptor, operatorConfig *config.Config, - installer install.Installer) *operationInstall { + installer install.InstallerChecker) *operationInstall { return &operationInstall{ logger: logger, @@ -45,10 +44,13 @@ func (o *operationInstall) Name() string { // Check checks whether install needs to be ran. // // If the installation directory already exists then it will not be ran. -func (o *operationInstall) Check(_ Application) (bool, error) { - installDir := o.program.Directory() - _, err := os.Stat(installDir) - return os.IsNotExist(err), nil +func (o *operationInstall) Check(ctx context.Context, _ Application) (bool, error) { + err := o.installer.Check(ctx, o.program.BinaryName(), o.program.Version(), o.program.Directory()) + if err != nil { + // don't return err, just state if Run should be called + return true, nil + } + return false, nil } // Run runs the operation @@ -59,5 +61,5 @@ func (o *operationInstall) Run(ctx context.Context, application Application) (er } }() - return o.installer.Install(o.program.BinaryName(), o.program.Version(), o.program.Directory()) + return o.installer.Install(ctx, o.program.BinaryName(), o.program.Version(), o.program.Directory()) } diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_remove.go b/x-pack/elastic-agent/pkg/agent/operation/operation_remove.go index 8b4457b440b..2f95f7ac50b 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_remove.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_remove.go @@ -26,7 +26,7 @@ func (o *operationRemove) Name() string { // Check checks whether remove needs to run. // // Always returns false. -func (o *operationRemove) Check(_ Application) (bool, error) { +func (o *operationRemove) Check(_ context.Context, _ Application) (bool, error) { return false, nil } diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_retryable.go b/x-pack/elastic-agent/pkg/agent/operation/operation_retryable.go index 96a0952a80d..f79eca617f8 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_retryable.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_retryable.go @@ -47,10 +47,10 @@ func (o *retryableOperations) Name() string { // examples: // - Start does not need to run if process is running // - Fetch does not need to run if package is already present -func (o *retryableOperations) Check(application Application) (bool, error) { +func (o *retryableOperations) Check(ctx context.Context, application Application) (bool, error) { for _, op := range o.operations { // finish early if at least one operation needs to be run or errored out - if run, err := op.Check(application); err != nil || run { + if run, err := op.Check(ctx, application); err != nil || run { return run, err } } @@ -71,7 +71,7 @@ func (o *retryableOperations) runOnce(application Application) func(context.Cont return ctx.Err() } - shouldRun, err := op.Check(application) + shouldRun, err := op.Check(ctx, application) if err != nil { return err } diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_start.go b/x-pack/elastic-agent/pkg/agent/operation/operation_start.go index 95354c9a00e..17a2d015c58 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_start.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_start.go @@ -47,11 +47,11 @@ func (o *operationStart) Name() string { // Only starts the application when in stopped state, any other state // and the application is handled by the life cycle inside of the `Application` // implementation. -func (o *operationStart) Check(application Application) (bool, error) { - if application.State().Status == state.Stopped { - return true, nil +func (o *operationStart) Check(_ context.Context, application Application) (bool, error) { + if application.Started() { + return false, nil } - return false, nil + return true, nil } // Run runs the operation diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_stop.go b/x-pack/elastic-agent/pkg/agent/operation/operation_stop.go index dcca55a1278..abe2acbff17 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_stop.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_stop.go @@ -36,7 +36,7 @@ func (o *operationStop) Name() string { // Check checks whether application needs to be stopped. // // If the application state is not stopped then stop should be performed. -func (o *operationStop) Check(application Application) (bool, error) { +func (o *operationStop) Check(_ context.Context, application Application) (bool, error) { if application.State().Status != state.Stopped { return true, nil } diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_uninstall.go b/x-pack/elastic-agent/pkg/agent/operation/operation_uninstall.go new file mode 100644 index 00000000000..1d30b639fa7 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_uninstall.go @@ -0,0 +1,55 @@ +// 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 operation + +import ( + "context" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/uninstall" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/state" +) + +// operationUninstall uninstalls a artifact from predefined location +type operationUninstall struct { + logger *logger.Logger + program Descriptor + uninstaller uninstall.Uninstaller +} + +func newOperationUninstall( + logger *logger.Logger, + program Descriptor, + uninstaller uninstall.Uninstaller) *operationUninstall { + + return &operationUninstall{ + logger: logger, + program: program, + uninstaller: uninstaller, + } +} + +// Name is human readable name identifying an operation +func (o *operationUninstall) Name() string { + return "operation-uninstall" +} + +// Check checks whether uninstall needs to be ran. +// +// Always true. +func (o *operationUninstall) Check(_ context.Context, _ Application) (bool, error) { + return true, nil +} + +// Run runs the operation +func (o *operationUninstall) Run(ctx context.Context, application Application) (err error) { + defer func() { + if err != nil { + application.SetState(state.Failed, err.Error()) + } + }() + + return o.uninstaller.Uninstall(ctx, o.program.BinaryName(), o.program.Version(), o.program.Directory()) +} diff --git a/x-pack/elastic-agent/pkg/agent/operation/operation_verify.go b/x-pack/elastic-agent/pkg/agent/operation/operation_verify.go index b54f559ce47..bc5d3d3b8cd 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operation_verify.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operation_verify.go @@ -43,7 +43,7 @@ func (o *operationVerify) Name() string { // Check checks whether verify needs to occur. // // Only if the artifacts exists does it need to be verified. -func (o *operationVerify) Check(_ Application) (bool, error) { +func (o *operationVerify) Check(_ context.Context, _ Application) (bool, error) { downloadConfig := o.operatorConfig.DownloadConfig fullPath, err := artifact.GetArtifactPath(o.program.BinaryName(), o.program.Version(), downloadConfig.OS(), downloadConfig.Arch(), downloadConfig.TargetDirectory) if err != nil { @@ -59,7 +59,7 @@ func (o *operationVerify) Check(_ Application) (bool, error) { } // Run runs the operation -func (o *operationVerify) Run(ctx context.Context, application Application) (err error) { +func (o *operationVerify) Run(_ context.Context, application Application) (err error) { defer func() { if err != nil { application.SetState(state.Failed, err.Error()) diff --git a/x-pack/elastic-agent/pkg/agent/operation/operator.go b/x-pack/elastic-agent/pkg/agent/operation/operator.go index e8b7562dff5..ed0b2d0ba43 100644 --- a/x-pack/elastic-agent/pkg/agent/operation/operator.go +++ b/x-pack/elastic-agent/pkg/agent/operation/operator.go @@ -18,6 +18,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/stateresolver" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/download" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/install" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/uninstall" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/app" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" @@ -53,9 +54,10 @@ type Operator struct { apps map[string]Application appsLock sync.Mutex - downloader download.Downloader - verifier download.Verifier - installer install.Installer + downloader download.Downloader + verifier download.Verifier + installer install.InstallerChecker + uninstaller uninstall.Uninstaller } // NewOperator creates a new operator, this operator holds @@ -68,7 +70,8 @@ func NewOperator( config *config.Config, fetcher download.Downloader, verifier download.Verifier, - installer install.Installer, + installer install.InstallerChecker, + uninstaller uninstall.Uninstaller, stateResolver *stateresolver.StateResolver, srv *server.Server, reporter state.Reporter, @@ -91,6 +94,7 @@ func NewOperator( downloader: fetcher, verifier: verifier, installer: installer, + uninstaller: uninstaller, stateResolver: stateResolver, srv: srv, apps: make(map[string]Application), @@ -175,6 +179,7 @@ func (o *Operator) start(p Descriptor, cfg map[string]interface{}) (err error) { func (o *Operator) stop(p Descriptor) (err error) { flow := []operation{ newOperationStop(o.logger, o.config), + newOperationUninstall(o.logger, p, o.uninstaller), } return o.runFlow(p, flow) @@ -205,7 +210,7 @@ func (o *Operator) runFlow(p Descriptor, operations []operation) error { return err } - shouldRun, err := op.Check(app) + shouldRun, err := op.Check(o.bgContext, app) if err != nil { return err } @@ -249,6 +254,7 @@ func (o *Operator) getApp(p Descriptor) (Application, error) { var a Application var err error if p.ServicePort() == 0 { + // Applications without service ports defined are ran as through the process application type. a, err = process.NewApplication( o.bgContext, p.ID(), @@ -262,6 +268,8 @@ func (o *Operator) getApp(p Descriptor) (Application, error) { o.reporter, o.monitor) } else { + // Service port is defined application is ran with service application type, with it fetching + // the connection credentials through the defined service port. a, err = service.NewApplication( o.bgContext, p.ID(), diff --git a/x-pack/elastic-agent/pkg/agent/program/program_test.go b/x-pack/elastic-agent/pkg/agent/program/program_test.go index 98bbdd7a962..b92785a11b3 100644 --- a/x-pack/elastic-agent/pkg/agent/program/program_test.go +++ b/x-pack/elastic-agent/pkg/agent/program/program_test.go @@ -461,7 +461,7 @@ func TestConfiguration(t *testing.T) { for _, program := range defPrograms { programConfig, err := ioutil.ReadFile(filepath.Join( "testdata", - name+"-"+strings.ToLower(program.Spec.Name)+".yml", + name+"-"+strings.ToLower(program.Spec.Cmd)+".yml", )) require.NoError(t, err) diff --git a/x-pack/elastic-agent/pkg/agent/program/spec.go b/x-pack/elastic-agent/pkg/agent/program/spec.go index 126dcb9da99..ed5fc61047c 100644 --- a/x-pack/elastic-agent/pkg/agent/program/spec.go +++ b/x-pack/elastic-agent/pkg/agent/program/spec.go @@ -27,13 +27,15 @@ var ErrMissingWhen = errors.New("program must define a 'When' expression") // NOTE: Current spec are build at compile time, we want to revisit that to allow other program // to register their spec in a secure way. type Spec struct { - Name string `yaml:"name"` - ServicePort int `yaml:"service,omitempty"` - Cmd string `yaml:"cmd"` - Args []string `yaml:"args"` - Rules *transpiler.RuleList `yaml:"rules"` - PostInstallSteps *transpiler.StepList `yaml:"post_install"` - When string `yaml:"when"` + Name string `yaml:"name"` + ServicePort int `yaml:"service,omitempty"` + Cmd string `yaml:"cmd"` + Args []string `yaml:"args"` + Rules *transpiler.RuleList `yaml:"rules"` + CheckInstallSteps *transpiler.StepList `yaml:"check_install"` + PostInstallSteps *transpiler.StepList `yaml:"post_install"` + PreUninstallSteps *transpiler.StepList `yaml:"pre_uninstall"` + When string `yaml:"when"` } // ReadSpecs reads all the specs that match the provided globbing path. diff --git a/x-pack/elastic-agent/pkg/agent/program/spec_test.go b/x-pack/elastic-agent/pkg/agent/program/spec_test.go index a231bdda567..df847de7aff 100644 --- a/x-pack/elastic-agent/pkg/agent/program/spec_test.go +++ b/x-pack/elastic-agent/pkg/agent/program/spec_test.go @@ -43,10 +43,16 @@ func TestSerialization(t *testing.T) { "log", ), ), + CheckInstallSteps: transpiler.NewStepList( + transpiler.ExecFile(25, "app", "verify", "--installed"), + ), PostInstallSteps: transpiler.NewStepList( transpiler.DeleteFile("d-1", true), transpiler.MoveFile("m-1", "m-2", false), ), + PreUninstallSteps: transpiler.NewStepList( + transpiler.ExecFile(30, "app", "uninstall", "--force"), + ), When: "1 == 1", } yml := `name: hello @@ -87,6 +93,13 @@ rules: key: type values: - log +check_install: +- exec_file: + path: app + args: + - verify + - --installed + timeout: 25 post_install: - delete_file: path: d-1 @@ -95,6 +108,13 @@ post_install: path: m-1 target: m-2 fail_on_missing: false +pre_uninstall: +- exec_file: + path: app + args: + - uninstall + - --force + timeout: 30 when: 1 == 1 ` t.Run("serialization", func(t *testing.T) { diff --git a/x-pack/elastic-agent/pkg/agent/program/supported.go b/x-pack/elastic-agent/pkg/agent/program/supported.go index 32e3d83334c..bdc9c951fbc 100644 --- a/x-pack/elastic-agent/pkg/agent/program/supported.go +++ b/x-pack/elastic-agent/pkg/agent/program/supported.go @@ -13,15 +13,15 @@ import ( ) var Supported []Spec -var SupportedMap map[string]bool +var SupportedMap map[string]Spec func init() { // Packed Files // spec/endpoint.yml // spec/filebeat.yml // spec/metricbeat.yml - unpacked := packer.MustUnpack("eJzsWE2To7gZvudnzDWpBMTghFTtwdCLALvpMe5GQjckuQFbgKvBH5DKf08JDDb27O7MZDenHLragD7ej+d93kf616dqv2F/2xR8X2ZF/dcmF5/++Ynmdk1eyyRA+o5BY0+L1YIVYUXw88zKlITk4jPWfIXlYUpfy2SjKYvL+4rDsMEaSakTCtaWC9eaJ67jCwrDLYdG85KZe1qYKnee+2+wFpvXMuGOOJFVmch3PLcrjsL2Jemfl2tT0Dw4USAOfF4ulmszj9FZIdh7vx/LQFgR5CtUc2cuFAfmhAo79d9iEOovmVzLzigMd1Y2T1zLBBE6q71v8nleu07QcPQ28elxfe/WvpzCUPD5+Dz10VISqoWKtMVK9loMw8NLZroR9k1a7EoPkIqgUPGaXeJlUbKGdrvGgaD4ufRwTWOknzgOWoyVvWtFuftzmjJFTykKWwbtLVknxbC+15wSD4RVhH0lRn5LkN1EICkWq/KnT3/p0/2eiQ3dxA/pzkVFkCcivFpEQN29ZF2YRZSHKZ/vU5bz9iUzqZuptpudErfwBXfC0zIXFV3rY1i/IJl+X3Rj7scW0i2zinAglnl4iJBXEbQySG5XDLxlS2ueLd/6/xTZhwhxQVF44JZeUxCILzippctxo/Yht9zKtdw6WMv/Xh0hPSUgrAnSldv1ueOpZD0ZW1HAixjpxTI/C56H1RcUiKgIC1covwn3LsX5XkRa8B4jfUdwcoGPqWywKV4yU67fWklZSxjGebjlttFyxxMRUt+Z4x0jELYMGCO0KNDfI2AcSH7eR9qqhy8IG24bKSkCwS7julJ5LRMKjYKdunKoIzwf9j9FOCgfyszxVAonJbWlmqljYFfUNhSqGlWMfWX43s8Jji+ZOdjcXsrjF2zVj1SbL8a5lik20BfMWc1cu8qYFjQE2TVrutjvSZc3b8RMjxGzocAXTPOPrHjO8OrOVi04YnDeM201u7UllqXyWia3MR1K/vv9GGOeEUSOLO9pgHVYUsUQH1J4R7nnUJpD7ml7fTf63OdIbJygiZCv9D4E73Ff4r+QN//OXnPPYVg/+jHds8ebmrKngXrNlMPkgQqv+L5SXoT0lkJbIa/lbR4HuzpcT2KH7BO7ocbxvcRl2+PzdrxrdTXRcUpfH2xxO8+FIejqFpA9hW8zF56P5DRdm2AiaLE6Mi3YxejzzHV8hUBxuNtH1vOBW4YSafOZ6wRbNv/6OhwFp5fMVIkzv7PlfCSNsaPA/yBYtpLgGIF6rMFbX5eaJwgULdb8impc+jVznf7do//syDTRynkylxvs38Shw1lNNSIw6NribR4+CN7NXCdsSWgcOQ5OHK++fR4MUg7tlvzYnEPXI7B/ipAvrpjxBEUGIKHRjZ/UZN9qdxEOUgZCZZnLdvX2R8uEaSx7jIzPMtc3eB8xsVybQ55vY7OL8fPNWn3OrvOf2/E31AWHpKLade/YCRQG9y0F1/1p+wx8ZKsECuU6155x7Ikej0HJ0dWGCJwlPnOqhQfiePp1Dj9SwCuCTTUqfDW6+v8RIfIRrf63EikCxmnEDyQNBcrDuhz+Y+ZCWddhRpCt9DxF9tQJBNs+5n/EhNXjZdAlD3JqjEktNriTZcIqeEkkN3QySSgEqe3CCv+OwHX/ZX6xT1WNxXqee70ce5EczzQzjcBb6QFD5Y6pcotVrsUFhXbLodgyEKYs98tOal1q32tO0rYiBnYeg5+LpTUvOh7UJP6n8ivf1B8Z+4oAe0WhwnKxvQiuLUWyIamCO94+Ahdhhr2OXFEzCpmW4EBllr6nUDkQpKdRfhbkSc0jdG7vRM8wVgbl1JH9bwm3XE1pbhcEqZIwDxQZO/Kqfl5iGaSqJnIvvPo14XZdH3egmY6FRkGAOJBGr7oieFJ3BHkqaTxuFeaR9QKr5dCuaN/UZIM78jx874BxQ1wM2kr8VEqbj8TpGtyBWIYkWYUg5bBBauWOBG4qMpYEr2ZSOFIQdAJpma86gpbNdlmImlr6LsZ+H1PL/T2EYU1w0Eht/oeKwwuWLuKjkbig6G2wJWe5UT+KxE4oPQqQ3xIVQwzw/8Xm7yE2CU4VltsSE12cMOhJbCTwC86/IjSnNt6970l4JOavNgms8T2H6TvLw4Lg9DTseWkMNyLQbCTOcPb5YwkuHKU9Dw2jJfLgqAVHtr0XpxfSF8ZdU/mFOdmtIBkPdBec3QkI2eTlQW/1w4ciGfcDA+eUw/CdA6HEttEQxMXGmU9wMOTgVpBc1pQ1vlk8GasvTqAz+PbnZVbtH2PU/3V7PJWJ14+9FSCTA8ekuU7GXg8Bkps5Ol8FGlTTGITvEfaa6F6gXTAy8gQYm+kNVgab5eH9+TuE4828q8hqCOp5u8PSxK9e3I2963vmQFuJ8HfuM8yx9C7GPy5oL/cvq2/fm2Nf4EFY9HdZHd98y8GSTvTBr/L60GOmYmfCA2OP/J57rsm6V5yN/oxClGDZQ41G1iIHqaDb8uFea+JPf8HT1eN/c8+17GzgDUHBnjWdcOtF8br73V3KyENNbLG9lfz006d//+k/AQAA//97aRE7") - SupportedMap = make(map[string]bool) + unpacked := packer.MustUnpack("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") + SupportedMap = make(map[string]Spec) for f, v := range unpacked { s, err := NewSpecFromBytes(v) @@ -29,6 +29,6 @@ func init() { panic("Cannot read spec from " + f) } Supported = append(Supported, s) - SupportedMap[strings.ToLower(s.Name)] = true + SupportedMap[strings.ToLower(s.Cmd)] = s } } diff --git a/x-pack/elastic-agent/pkg/agent/program/testdata/endpoint_basic-endpoint.yml b/x-pack/elastic-agent/pkg/agent/program/testdata/endpoint_basic-endpoint-security.yml similarity index 100% rename from x-pack/elastic-agent/pkg/agent/program/testdata/endpoint_basic-endpoint.yml rename to x-pack/elastic-agent/pkg/agent/program/testdata/endpoint_basic-endpoint-security.yml diff --git a/x-pack/elastic-agent/pkg/agent/program/testdata/single_config-endpoint.yml b/x-pack/elastic-agent/pkg/agent/program/testdata/single_config-endpoint-security.yml similarity index 100% rename from x-pack/elastic-agent/pkg/agent/program/testdata/single_config-endpoint.yml rename to x-pack/elastic-agent/pkg/agent/program/testdata/single_config-endpoint-security.yml diff --git a/x-pack/elastic-agent/pkg/agent/transpiler/rules.go b/x-pack/elastic-agent/pkg/agent/transpiler/rules.go index fe63f0e94e5..3cc94798e2d 100644 --- a/x-pack/elastic-agent/pkg/agent/transpiler/rules.go +++ b/x-pack/elastic-agent/pkg/agent/transpiler/rules.go @@ -263,7 +263,8 @@ func (r *CopyToListRule) Apply(ast *AST) error { targetList, ok := targetListNode.Value().(*List) if !ok { - return errors.New("target node is not a list") + // not a list; skip + return nil } for _, listItem := range targetList.value { diff --git a/x-pack/elastic-agent/pkg/agent/transpiler/steps.go b/x-pack/elastic-agent/pkg/agent/transpiler/steps.go index a5538a91a26..3d52ccacb34 100644 --- a/x-pack/elastic-agent/pkg/agent/transpiler/steps.go +++ b/x-pack/elastic-agent/pkg/agent/transpiler/steps.go @@ -5,11 +5,14 @@ package transpiler import ( + "context" "fmt" "os" + "os/exec" "path/filepath" "runtime" "strings" + "time" "gopkg.in/yaml.v2" ) @@ -26,14 +29,14 @@ func NewStepList(steps ...Step) *StepList { // Step is an execution step which needs to be run. type Step interface { - Execute(rootDir string) error + Execute(ctx context.Context, rootDir string) error } // Execute executes a list of steps. -func (r *StepList) Execute(rootDir string) error { +func (r *StepList) Execute(ctx context.Context, rootDir string) error { var err error for _, step := range r.Steps { - err = step.Execute(rootDir) + err = step.Execute(ctx, rootDir) if err != nil { return err } @@ -53,6 +56,8 @@ func (r *StepList) MarshalYAML() (interface{}, error) { name = "delete_file" case *MoveFileStep: name = "move_file" + case *ExecFileStep: + name = "exec_file" default: return nil, fmt.Errorf("unknown rule of type %T", step) @@ -105,6 +110,8 @@ func (r *StepList) UnmarshalYAML(unmarshal func(interface{}) error) error { s = &DeleteFileStep{} case "move_file": s = &MoveFileStep{} + case "exec_file": + s = &ExecFileStep{} default: return fmt.Errorf("unknown rule of type %s", name) } @@ -127,7 +134,7 @@ type DeleteFileStep struct { } // Execute executes delete file step. -func (r *DeleteFileStep) Execute(rootDir string) error { +func (r *DeleteFileStep) Execute(_ context.Context, rootDir string) error { path, isSubpath, err := joinPaths(rootDir, r.Path) if err != nil { return err @@ -169,7 +176,7 @@ type MoveFileStep struct { } // Execute executes move file step. -func (r *MoveFileStep) Execute(rootDir string) error { +func (r *MoveFileStep) Execute(_ context.Context, rootDir string) error { path, isSubpath, err := joinPaths(rootDir, r.Path) if err != nil { return err @@ -212,6 +219,57 @@ func MoveFile(path, target string, failOnMissing bool) *MoveFileStep { } } +// ExecFileStep executes a file. +type ExecFileStep struct { + Path string + Args []string + Timeout int +} + +// Execute executes file with provided arguments. +func (r *ExecFileStep) Execute(ctx context.Context, rootDir string) error { + path, isSubpath, err := joinPaths(rootDir, r.Path) + if err != nil { + return err + } + + if !isSubpath { + return fmt.Errorf("invalid path value for operation 'Exec': %s", path) + } + + // timeout defaults to 60 seconds + if r.Timeout == 0 { + r.Timeout = 60 + } + ctx, cancel := context.WithTimeout(ctx, time.Duration(r.Timeout)*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, path, r.Args...) + cmd.Env = nil + cmd.Dir = rootDir + output, err := cmd.Output() + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("operation 'Exec' timed out after %d seconds", r.Timeout) + } + if err != nil { + exitErr, ok := err.(*exec.ExitError) + if ok && exitErr.Stderr != nil && len(exitErr.Stderr) > 0 { + return fmt.Errorf("operation 'Exec' failed: %s", string(exitErr.Stderr)) + } + return fmt.Errorf("operation 'Exec' failed: %s", string(output)) + } + return nil +} + +// ExecFile creates a ExecFileStep +func ExecFile(timeoutSecs int, path string, args ...string) *ExecFileStep { + return &ExecFileStep{ + Path: path, + Args: args, + Timeout: timeoutSecs, + } +} + // joinPaths joins paths and returns true if path is subpath of rootDir func joinPaths(rootDir, path string) (string, bool, error) { rootDir = filepath.FromSlash(rootDir) diff --git a/x-pack/elastic-agent/pkg/artifact/install/dir/dir_checker.go b/x-pack/elastic-agent/pkg/artifact/install/dir/dir_checker.go new file mode 100644 index 00000000000..56ecffdc5c0 --- /dev/null +++ b/x-pack/elastic-agent/pkg/artifact/install/dir/dir_checker.go @@ -0,0 +1,24 @@ +// 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 dir + +import ( + "context" + "os" +) + +// Checker performs basic check that the install directory exists. +type Checker struct{} + +// NewChecker returns a new Checker. +func NewChecker() *Checker { + return &Checker{} +} + +// Check checks that the install directory exists. +func (*Checker) Check(_ context.Context, _, _, installDir string) error { + _, err := os.Stat(installDir) + return err +} diff --git a/x-pack/elastic-agent/pkg/artifact/install/hooks/hooks_installer.go b/x-pack/elastic-agent/pkg/artifact/install/hooks/hooks_installer.go index fd2f4e4bfdb..8df01fdae7a 100644 --- a/x-pack/elastic-agent/pkg/artifact/install/hooks/hooks_installer.go +++ b/x-pack/elastic-agent/pkg/artifact/install/hooks/hooks_installer.go @@ -5,53 +5,68 @@ package hooks import ( + "context" "strings" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/program" ) type embeddedInstaller interface { - Install(programName, version, installDir string) error + Install(ctx context.Context, programName, version, installDir string) error } -// Installer or zip packages -type Installer struct { +type embeddedChecker interface { + Check(ctx context.Context, programName, version, installDir string) error +} + +// InstallerChecker runs the PostInstallSteps after running the embedded installer +// and runs the InstallerCheckSteps after running the embedded installation checker. +type InstallerChecker struct { installer embeddedInstaller + checker embeddedChecker } -// NewInstaller creates an installer able to install zip packages -func NewInstaller(i embeddedInstaller) (*Installer, error) { - return &Installer{ +// NewInstallerChecker creates a new InstallerChecker +func NewInstallerChecker(i embeddedInstaller, c embeddedChecker) (*InstallerChecker, error) { + return &InstallerChecker{ installer: i, + checker: c, }, nil } -// Install performs installation of program in a specific version. -// It expects package to be already downloaded. -func (i *Installer) Install(programName, version, installDir string) error { - if err := i.installer.Install(programName, version, installDir); err != nil { +// Install performs installation of program in a specific version, then runs the +// PostInstallSteps for the program if defined. +func (i *InstallerChecker) Install(ctx context.Context, programName, version, installDir string) error { + if err := i.installer.Install(ctx, programName, version, installDir); err != nil { return err } // post install hooks - nameLower := strings.ToLower(programName) - _, isSupported := program.SupportedMap[nameLower] - if !isSupported { + spec, ok := program.SupportedMap[strings.ToLower(programName)] + if !ok { return nil } + if spec.PostInstallSteps != nil { + return spec.PostInstallSteps.Execute(ctx, installDir) + } + return nil +} - for _, spec := range program.Supported { - if strings.ToLower(spec.Name) != nameLower { - continue - } - - if spec.PostInstallSteps != nil { - return spec.PostInstallSteps.Execute(installDir) - } - - // only one spec for type - break +// Check performs installation check of program to ensure that it is already installed, then +// runs the InstallerCheckSteps to ensure that the installation is valid. +func (i *InstallerChecker) Check(ctx context.Context, programName, version, installDir string) error { + err := i.checker.Check(ctx, programName, version, installDir) + if err != nil { + return err } + // installer check steps + spec, ok := program.SupportedMap[strings.ToLower(programName)] + if !ok { + return nil + } + if spec.CheckInstallSteps != nil { + return spec.CheckInstallSteps.Execute(ctx, installDir) + } return nil } diff --git a/x-pack/elastic-agent/pkg/artifact/install/installer.go b/x-pack/elastic-agent/pkg/artifact/install/installer.go index 4856468ac83..f04e7a4238e 100644 --- a/x-pack/elastic-agent/pkg/artifact/install/installer.go +++ b/x-pack/elastic-agent/pkg/artifact/install/installer.go @@ -5,9 +5,12 @@ package install import ( + "context" "errors" "runtime" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/install/dir" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/install/hooks" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/install/tar" @@ -24,7 +27,15 @@ type Installer interface { // Install installs an artifact and returns // location of the installed program // error if something went wrong - Install(programName, version, installDir string) error + Install(ctx context.Context, programName, version, installDir string) error +} + +// InstallerChecker is an interface that installs but also checks for valid installation. +type InstallerChecker interface { + Installer + + // Check checks if the installation is good. + Check(ctx context.Context, programName, version, installDir string) error } // NewInstaller returns a correct installer associated with a @@ -32,7 +43,7 @@ type Installer interface { // - rpm -> rpm installer // - deb -> deb installer // - binary -> zip installer on windows, tar installer on linux and mac -func NewInstaller(config *artifact.Config) (Installer, error) { +func NewInstaller(config *artifact.Config) (InstallerChecker, error) { if config == nil { return nil, ErrConfigNotProvided } @@ -49,5 +60,5 @@ func NewInstaller(config *artifact.Config) (Installer, error) { return nil, err } - return hooks.NewInstaller(installer) + return hooks.NewInstallerChecker(installer, dir.NewChecker()) } diff --git a/x-pack/elastic-agent/pkg/artifact/install/tar/tar_installer.go b/x-pack/elastic-agent/pkg/artifact/install/tar/tar_installer.go index 52d49f627f5..f73ace5765a 100644 --- a/x-pack/elastic-agent/pkg/artifact/install/tar/tar_installer.go +++ b/x-pack/elastic-agent/pkg/artifact/install/tar/tar_installer.go @@ -7,6 +7,7 @@ package tar import ( "archive/tar" "compress/gzip" + "context" "fmt" "io" "os" @@ -31,7 +32,7 @@ func NewInstaller(config *artifact.Config) (*Installer, error) { // Install performs installation of program in a specific version. // It expects package to be already downloaded. -func (i *Installer) Install(programName, version, _ string) error { +func (i *Installer) Install(_ context.Context, programName, version, installDir string) error { artifactPath, err := artifact.GetArtifactPath(programName, version, i.config.OS(), i.config.Arch(), i.config.TargetDirectory) if err != nil { return err @@ -43,7 +44,13 @@ func (i *Installer) Install(programName, version, _ string) error { } defer f.Close() - return unpack(f, i.config.InstallPath) + // cleanup install directory before unpack + _, err = os.Stat(installDir) + if err == nil || os.IsExist(err) { + os.RemoveAll(installDir) + } + + return unpack(f, installDir) } diff --git a/x-pack/elastic-agent/pkg/artifact/install/zip/zip_installer.go b/x-pack/elastic-agent/pkg/artifact/install/zip/zip_installer.go index 5293e49f016..451cd701627 100644 --- a/x-pack/elastic-agent/pkg/artifact/install/zip/zip_installer.go +++ b/x-pack/elastic-agent/pkg/artifact/install/zip/zip_installer.go @@ -6,6 +6,7 @@ package zip import ( "archive/zip" + "context" "fmt" "os" "os/exec" @@ -34,12 +35,18 @@ func NewInstaller(config *artifact.Config) (*Installer, error) { // Install performs installation of program in a specific version. // It expects package to be already downloaded. -func (i *Installer) Install(programName, version, installDir string) error { +func (i *Installer) Install(_ context.Context, programName, version, installDir string) error { artifactPath, err := artifact.GetArtifactPath(programName, version, i.config.OS(), i.config.Arch(), i.config.TargetDirectory) if err != nil { return err } + // cleanup install directory before unzip + _, err = os.Stat(installDir) + if err == nil || os.IsExist(err) { + os.RemoveAll(installDir) + } + if err := i.unzip(artifactPath, programName, version); err != nil { return err } @@ -57,7 +64,7 @@ func (i *Installer) Install(programName, version, installDir string) error { } } - return i.runInstall(programName, version, installDir) + return nil } func (i *Installer) unzip(artifactPath, programName, version string) error { @@ -70,13 +77,6 @@ func (i *Installer) unzip(artifactPath, programName, version string) error { return installCmd.Run() } -func (i *Installer) runInstall(programName, version, installPath string) error { - powershellCmd := fmt.Sprintf(powershellCmdTemplate, installPath, programName) - installCmd := exec.Command("powershell", "-command", powershellCmd) - - return installCmd.Run() -} - // retrieves root directory from zip archive func (i *Installer) getRootDir(zipPath string) (dir string, err error) { defer func() { diff --git a/x-pack/elastic-agent/pkg/artifact/uninstall/hooks/hooks_uninstaller.go b/x-pack/elastic-agent/pkg/artifact/uninstall/hooks/hooks_uninstaller.go new file mode 100644 index 00000000000..250c703be66 --- /dev/null +++ b/x-pack/elastic-agent/pkg/artifact/uninstall/hooks/hooks_uninstaller.go @@ -0,0 +1,41 @@ +// 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 hooks + +import ( + "context" + "strings" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/program" +) + +type embeddedUninstaller interface { + Uninstall(ctx context.Context, programName, version, installDir string) error +} + +// Uninstaller that executes PreUninstallSteps +type Uninstaller struct { + uninstaller embeddedUninstaller +} + +// NewUninstaller creates an uninstaller that executes PreUninstallSteps +func NewUninstaller(i embeddedUninstaller) (*Uninstaller, error) { + return &Uninstaller{ + uninstaller: i, + }, nil +} + +// Uninstall performs the execution of the PreUninstallSteps +func (i *Uninstaller) Uninstall(ctx context.Context, programName, version, installDir string) error { + // pre uninstall hooks + spec, ok := program.SupportedMap[strings.ToLower(programName)] + if !ok { + return nil + } + if spec.PreUninstallSteps != nil { + return spec.PreUninstallSteps.Execute(ctx, installDir) + } + return i.uninstaller.Uninstall(ctx, programName, version, installDir) +} diff --git a/x-pack/elastic-agent/pkg/artifact/uninstall/uninstaller.go b/x-pack/elastic-agent/pkg/artifact/uninstall/uninstaller.go new file mode 100644 index 00000000000..f2965f8dcf4 --- /dev/null +++ b/x-pack/elastic-agent/pkg/artifact/uninstall/uninstaller.go @@ -0,0 +1,28 @@ +// 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 uninstall + +import ( + "context" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact/uninstall/hooks" +) + +// Uninstaller is an interface allowing un-installation of an artifact +type Uninstaller interface { + // Uninstall uninstalls an artifact. + Uninstall(ctx context.Context, programName, version, installDir string) error +} + +// NewUninstaller returns a correct uninstaller. +func NewUninstaller() (Uninstaller, error) { + return hooks.NewUninstaller(&nilUninstaller{}) +} + +type nilUninstaller struct{} + +func (*nilUninstaller) Uninstall(_ context.Context, _, _, _ string) error { + return nil +} diff --git a/x-pack/elastic-agent/pkg/core/plugin/process/app.go b/x-pack/elastic-agent/pkg/core/plugin/process/app.go index d68f6c5026f..7696665bbd0 100644 --- a/x-pack/elastic-agent/pkg/core/plugin/process/app.go +++ b/x-pack/elastic-agent/pkg/core/plugin/process/app.go @@ -120,6 +120,11 @@ func (a *Application) Name() string { return a.name } +// Started returns true if the application is started. +func (a *Application) Started() bool { + return a.state.Status != state.Stopped +} + // Stop stops the current application. func (a *Application) Stop() { a.appLock.Lock() diff --git a/x-pack/elastic-agent/pkg/core/plugin/process/status.go b/x-pack/elastic-agent/pkg/core/plugin/process/status.go index f5d015ed45d..d8ba5039e29 100644 --- a/x-pack/elastic-agent/pkg/core/plugin/process/status.go +++ b/x-pack/elastic-agent/pkg/core/plugin/process/status.go @@ -17,7 +17,7 @@ import ( // OnStatusChange is the handler called by the GRPC server code. // -// It updates the status of the application and handles restarting the application is needed. +// It updates the status of the application and handles restarting the application if needed. func (a *Application) OnStatusChange(s *server.ApplicationState, status proto.StateObserved_Status, msg string) { a.appLock.Lock() defer a.appLock.Unlock() diff --git a/x-pack/elastic-agent/pkg/core/plugin/service/app.go b/x-pack/elastic-agent/pkg/core/plugin/service/app.go index 82296c78a82..d4975d828c6 100644 --- a/x-pack/elastic-agent/pkg/core/plugin/service/app.go +++ b/x-pack/elastic-agent/pkg/core/plugin/service/app.go @@ -125,6 +125,11 @@ func (a *Application) Name() string { return a.name } +// Started returns true if the application is started. +func (a *Application) Started() bool { + return a.srvState != nil +} + // SetState sets the status of the application. func (a *Application) SetState(status state.Status, msg string) { a.appLock.Lock() diff --git a/x-pack/elastic-agent/spec/endpoint.yml b/x-pack/elastic-agent/spec/endpoint.yml index df06d40446a..e864c182e6e 100644 --- a/x-pack/elastic-agent/spec/endpoint.yml +++ b/x-pack/elastic-agent/spec/endpoint.yml @@ -1,4 +1,27 @@ -name: Endpoint +name: Endpoint Security +cmd: endpoint-security +service: 6788 +check_install: +- exec_file: + path: "endpoint-security" + args: + - "verify" + timeout: 30 +post_install: +- exec_file: + path: "endpoint-security" + args: + - "install" + - "--upgrade" + - "--resources" + - "endpoint-security-resources.zip" + timeout: 120 +pre_uninstall: +- exec_file: + path: "endpoint-security" + args: + - "uninstall" + timeout: 120 rules: - fix_stream: {}