diff --git a/NOTICE.txt b/NOTICE.txt index 748fe0f5e981..527c13043795 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -10907,6 +10907,36 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Dependency : github.com/blakerouse/service +Version: v1.1.1-0.20200924160513-057808572ffa +Licence type (autodetected): Zlib +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/blakerouse/service@v1.1.1-0.20200924160513-057808572ffa/LICENSE: + +Copyright (c) 2015 Daniel Theophanes + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. + + -------------------------------------------------------------------------------- Dependency : github.com/lib/pq Version: v1.1.2-0.20190507191818-2ff3cb3adc01 @@ -11843,6 +11873,37 @@ Contents of probable licence file $GOMODCACHE/github.com/oklog/ulid@v1.3.1/LICEN limitations under the License. +-------------------------------------------------------------------------------- +Dependency : github.com/otiai10/copy +Version: v1.2.0 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/otiai10/copy@v1.2.0/LICENSE: + +The MIT License (MIT) + +Copyright (c) 2018 otiai10 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + -------------------------------------------------------------------------------- Dependency : github.com/pierrre/gotestcover Version: v0.0.0-20160517101806-924dca7d15f0 @@ -34009,6 +34070,54 @@ Contents of probable licence file $GOMODCACHE/github.com/opencontainers/runtime- limitations under the License. +-------------------------------------------------------------------------------- +Dependency : github.com/otiai10/curr +Version: v1.0.0 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/otiai10/curr@v1.0.0/LICENSE: + +The MIT License (MIT) + +Copyright (c) 2020 Hiromu Ochiai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +-------------------------------------------------------------------------------- +Dependency : github.com/otiai10/mint +Version: v1.3.1 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/otiai10/mint@v1.3.1/LICENSE: + +Copyright 2017 otiai10 (Hiromu OCHIAI) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -------------------------------------------------------------------------------- Dependency : github.com/oxtoacart/bpool Version: v0.0.0-20150712133111-4e1c5567d7c2 diff --git a/dev-tools/notice/rules.json b/dev-tools/notice/rules.json index 73ce763cdaef..c9638a9c6cf8 100644 --- a/dev-tools/notice/rules.json +++ b/dev-tools/notice/rules.json @@ -9,7 +9,8 @@ "ISC", "MIT", "MPL-2.0", - "Public Domain" + "Public Domain", + "Zlib" ], "maybelist": [ "EPL-1.0", diff --git a/dev-tools/packaging/templates/linux/elastic-agent.sh.tmpl b/dev-tools/packaging/templates/linux/elastic-agent.sh.tmpl index 744abc05702d..835b59553243 100644 --- a/dev-tools/packaging/templates/linux/elastic-agent.sh.tmpl +++ b/dev-tools/packaging/templates/linux/elastic-agent.sh.tmpl @@ -6,6 +6,5 @@ exec /usr/share/{{.BeatName}}/bin/{{.BeatName}} \ --path.home /var/lib/{{.BeatName}} \ --path.config /etc/{{.BeatName}} \ - --path.data /var/lib/{{.BeatName}}/data \ --path.logs /var/log/{{.BeatName}} \ "$@" diff --git a/go.mod b/go.mod index 2fafe7508792..09cd086cbee1 100644 --- a/go.mod +++ b/go.mod @@ -107,6 +107,7 @@ require ( github.com/josephspurrier/goversioninfo v0.0.0-20190209210621-63e6d1acd3dd github.com/jpillora/backoff v1.0.0 // indirect github.com/jstemmer/go-junit-report v0.9.1 + github.com/kardianos/service v1.1.0 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01 github.com/magefile/mage v1.10.0 @@ -123,6 +124,7 @@ require ( github.com/oklog/ulid v1.3.1 github.com/opencontainers/go-digest v1.0.0-rc1.0.20190228220655-ac19fd6e7483 // indirect github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 // indirect + github.com/otiai10/copy v1.2.0 github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 @@ -195,6 +197,7 @@ replace ( github.com/fsnotify/fsnotify => github.com/adriansr/fsnotify v0.0.0-20180417234312-c9bbe1f46f1d github.com/google/gopacket => github.com/adriansr/gopacket v1.1.18-0.20200327165309-dd62abfa8a41 github.com/insomniacslk/dhcp => github.com/elastic/dhcp v0.0.0-20200227161230-57ec251c7eb3 // indirect + github.com/kardianos/service => github.com/blakerouse/service v1.1.1-0.20200924160513-057808572ffa github.com/tonistiigi/fifo => github.com/containerd/fifo v0.0.0-20190816180239-bda0ff6ed73c golang.org/x/tools => golang.org/x/tools v0.0.0-20200602230032-c00d67ef29d0 // release 1.14 ) diff --git a/go.sum b/go.sum index 44f6eeb2ba02..031f1faa0955 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blakerouse/service v1.1.1-0.20200924160513-057808572ffa h1:aXHPZwx8Y5z8r+1WPylnu095usTf6QSshaHs6nVMBc0= +github.com/blakerouse/service v1.1.1-0.20200924160513-057808572ffa/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc= github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2 h1:oMCHnXa6CCCafdPDbMh/lWRhRByN0VFLvv+g+ayx1SI= github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= @@ -563,6 +565,14 @@ github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rm github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -817,6 +827,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/libbeat/common/cli/input.go b/libbeat/common/cli/input.go new file mode 100644 index 000000000000..a6a516fd3d49 --- /dev/null +++ b/libbeat/common/cli/input.go @@ -0,0 +1,43 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package cli + +import ( + "bufio" + "fmt" + "io" + "os" + + "github.com/pkg/errors" +) + +// ReadInput shows the text and ask the user to provide input. +func ReadInput(prompt string) (string, error) { + reader := bufio.NewReader(os.Stdin) + return input(reader, prompt) +} + +func input(r io.Reader, prompt string) (string, error) { + reader := bufio.NewScanner(r) + fmt.Print(prompt + " ") + + if !reader.Scan() { + return "", errors.New("error reading user input") + } + return reader.Text(), nil +} diff --git a/libbeat/common/cli/input_test.go b/libbeat/common/cli/input_test.go new file mode 100644 index 000000000000..de87b5efe2ae --- /dev/null +++ b/libbeat/common/cli/input_test.go @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package cli + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadInput(t *testing.T) { + tests := []struct { + name string + input string + res string + }{ + { + name: "Question 1?", + input: "\n", + res: "", + }, + { + name: "Question 2?", + input: "full string input\n", + res: "full string input", + }, + { + name: "Question 3?", + input: "123456789\n", + res: "123456789", + }, + { + name: "Question 4?", + input: "false\n", + res: "false", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r := strings.NewReader(test.input) + result, err := input(r, test.name) + assert.NoError(t, err) + assert.Equal(t, test.res, result) + }) + } +} diff --git a/x-pack/elastic-agent/CHANGELOG.next.asciidoc b/x-pack/elastic-agent/CHANGELOG.next.asciidoc index c466d0c656d6..54cb492e0f4b 100644 --- a/x-pack/elastic-agent/CHANGELOG.next.asciidoc +++ b/x-pack/elastic-agent/CHANGELOG.next.asciidoc @@ -25,3 +25,4 @@ - Add support for dynamic inputs with providers and `{{variable|"default"}}` substitution. {pull}20839[20839] - Add support for EQL based condition on inputs {pull}20994[20994] - Send `fleet.host.id` to Endpoint Security {pull}21042[21042] +- Add `install` and `uninstall` subcommands {pull}21206[21206] diff --git a/x-pack/elastic-agent/magefile.go b/x-pack/elastic-agent/magefile.go index 7296e8189be5..4fa067f8f8b7 100644 --- a/x-pack/elastic-agent/magefile.go +++ b/x-pack/elastic-agent/magefile.go @@ -658,6 +658,7 @@ func buildVars() map[string]string { if isDevFlag, devFound := os.LookupEnv(devEnv); devFound { if isDev, err := strconv.ParseBool(isDevFlag); err == nil && isDev { vars["github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/release.allowEmptyPgp"] = "true" + vars["github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/release.allowUpgrade"] = "true" } } diff --git a/x-pack/elastic-agent/pkg/agent/application/paths/paths.go b/x-pack/elastic-agent/pkg/agent/application/paths/paths.go index 48544ec7593d..b646f3796ba8 100644 --- a/x-pack/elastic-agent/pkg/agent/application/paths/paths.go +++ b/x-pack/elastic-agent/pkg/agent/application/paths/paths.go @@ -6,121 +6,87 @@ package paths import ( "flag" + "fmt" "os" "path/filepath" - "runtime" - "sync" + "strings" - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/release" ) var ( - homePath string - configPath string - dataPath string - logsPath string - serviceName string - - overridesLoader sync.Once + topPath string + configPath string + logsPath string ) func init() { - initialHome := initialHome() + topPath = initialTop() + configPath = topPath + logsPath = topPath fs := flag.CommandLine - fs.StringVar(&homePath, "path.home", initialHome, "Agent root path") - fs.StringVar(&configPath, "path.config", initialHome, "Config path is the directory Agent looks for its config file") - fs.StringVar(&dataPath, "path.data", filepath.Join(initialHome, "data"), "Data path contains Agent managed binaries") - fs.StringVar(&logsPath, "path.logs", initialHome, "Logs path contains Agent log output") + fs.StringVar(&topPath, "path.home", topPath, "Agent root path") + fs.StringVar(&configPath, "path.config", configPath, "Config path is the directory Agent looks for its config file") + fs.StringVar(&logsPath, "path.logs", logsPath, "Logs path contains Agent log output") } -// UpdatePaths update paths based on changes in paths file. -func UpdatePaths() { - getOverrides() -} - -func getOverrides() { - type paths struct { - HomePath string `config:"path.home" yaml:"path.home"` - ConfigPath string `config:"path.config" yaml:"path.config"` - DataPath string `config:"path.data" yaml:"path.data"` - LogsPath string `config:"path.logs" yaml:"path.logs"` - ServiceName string `config:"path.service_name" yaml:"path.service_name"` - } - - defaults := &paths{ - HomePath: homePath, - ConfigPath: configPath, - DataPath: dataPath, - LogsPath: logsPath, - } - - pathsFile := filepath.Join(dataPath, "paths.yml") - rawConfig, err := config.LoadYAML(pathsFile) - if err != nil { - return - } - - rawConfig.Unpack(defaults) - homePath = defaults.HomePath - configPath = defaults.ConfigPath - dataPath = defaults.DataPath - logsPath = defaults.LogsPath - serviceName = defaults.ServiceName -} - -// ServiceName return predefined service name if defined by initial call. -func ServiceName() string { - // needs to do this at this place because otherwise it will - // get overwritten by flags behavior. - overridesLoader.Do(getOverrides) - return serviceName +// Top returns the top directory for Elastic Agent, all the versioned +// home directories live under this top-level/data/elastic-agent-${hash} +func Top() string { + return topPath } // Home returns a directory where binary lives -// Executable is not supported on nacl. func Home() string { - overridesLoader.Do(getOverrides) - return homePath + return versionedHome(topPath) } // Config returns a directory where configuration file lives func Config() string { - overridesLoader.Do(getOverrides) return configPath } // Data returns the data directory for Agent func Data() string { - overridesLoader.Do(getOverrides) - return dataPath + return filepath.Join(Top(), "data") } // Logs returns a the log directory for Agent func Logs() string { - overridesLoader.Do(getOverrides) return logsPath } +// initialTop returns the initial top-level path for the binary +// +// When nested in top-level/data/elastic-agent-${hash}/ the result is top-level/. +func initialTop() string { + exePath := retrieveExecutablePath() + if insideData(exePath) { + return filepath.Dir(filepath.Dir(exePath)) + } + return exePath +} + +// retrieveExecutablePath returns the executing binary, even if the started binary was a symlink func retrieveExecutablePath() string { execPath, err := os.Executable() if err != nil { panic(err) } - evalPath, err := filepath.EvalSymlinks(execPath) if err != nil { panic(err) } - return filepath.Dir(evalPath) } -func initialHome() string { - exePath := retrieveExecutablePath() - if runtime.GOOS == "windows" { - return exePath - } +// insideData returns true when the exePath is inside of the current Agents data path. +func insideData(exePath string) bool { + expectedPath := filepath.Join("data", fmt.Sprintf("elastic-agent-%s", release.ShortCommit())) + return strings.HasSuffix(exePath, expectedPath) +} - return filepath.Dir(filepath.Dir(exePath)) // is two level up the executable (symlink evaluated) +func versionedHome(base string) string { + return filepath.Join(base, "data", fmt.Sprintf("elastic-agent-%s", release.ShortCommit())) } diff --git a/x-pack/elastic-agent/pkg/agent/application/upgrade/step_mark.go b/x-pack/elastic-agent/pkg/agent/application/upgrade/step_mark.go index 0d8253bb9ca8..53920e6ecffa 100644 --- a/x-pack/elastic-agent/pkg/agent/application/upgrade/step_mark.go +++ b/x-pack/elastic-agent/pkg/agent/application/upgrade/step_mark.go @@ -6,11 +6,8 @@ package upgrade import ( "context" - "fmt" "io/ioutil" - "os" "path/filepath" - "runtime" "time" "gopkg.in/yaml.v2" @@ -41,10 +38,6 @@ type updateMarker struct { // markUpgrade marks update happened so we can handle grace period func (h *Upgrader) markUpgrade(ctx context.Context, hash string, action *fleetapi.ActionUpgrade) error { - if err := updateHomePath(hash); err != nil { - return err - } - prevVersion := release.Version() prevHash := release.Commit() if len(prevHash) > hashLen { @@ -69,55 +62,10 @@ func (h *Upgrader) markUpgrade(ctx context.Context, hash string, action *fleetap return errors.New(err, errors.TypeFilesystem, "failed to create update marker file", errors.M(errors.MetaKeyPath, markerPath)) } - activeCommitPath := filepath.Join(paths.Config(), agentCommitFile) + activeCommitPath := filepath.Join(paths.Top(), agentCommitFile) if err := ioutil.WriteFile(activeCommitPath, []byte(hash), 0644); err != nil { return errors.New(err, errors.TypeFilesystem, "failed to update active commit", errors.M(errors.MetaKeyPath, activeCommitPath)) } return nil } - -func updateHomePath(hash string) error { - if err := createPathsSymlink(hash); err != nil { - return errors.New(err, errors.TypeFilesystem, "failed to create paths symlink") - } - - pathsMap := make(map[string]string) - pathsFilepath := filepath.Join(paths.Data(), "paths.yml") - - pathsBytes, err := ioutil.ReadFile(pathsFilepath) - if err != nil { - return errors.New(err, errors.TypeConfig, "failed to read paths file") - } - - if err := yaml.Unmarshal(pathsBytes, &pathsMap); err != nil { - return errors.New(err, errors.TypeConfig, "failed to parse paths file") - } - - pathsMap["path.home"] = filepath.Join(filepath.Dir(paths.Home()), fmt.Sprintf("%s-%s", agentName, hash)) - - pathsBytes, err = yaml.Marshal(pathsMap) - if err != nil { - return errors.New(err, errors.TypeConfig, "failed to marshal paths file") - } - - return ioutil.WriteFile(pathsFilepath, pathsBytes, 0740) -} - -func createPathsSymlink(hash string) error { - // only on windows, as windows resolves PWD using symlinks in a different way. - // we create symlink for each versioned agent inside `data/` directory - // on other systems path is shared - if runtime.GOOS != "windows" { - return nil - } - - dir := filepath.Join(paths.Data(), fmt.Sprintf("%s-%s", agentName, hash)) - versionedPath := filepath.Join(dir, "data", "paths.yml") - if err := os.MkdirAll(filepath.Dir(versionedPath), 0700); err != nil { - return err - } - - pathsCfgPath := filepath.Join(paths.Data(), "paths.yml") - return os.Symlink(pathsCfgPath, versionedPath) -} diff --git a/x-pack/elastic-agent/pkg/agent/application/upgrade/step_relink.go b/x-pack/elastic-agent/pkg/agent/application/upgrade/step_relink.go index 48d22de36cfe..7cbd78d48499 100644 --- a/x-pack/elastic-agent/pkg/agent/application/upgrade/step_relink.go +++ b/x-pack/elastic-agent/pkg/agent/application/upgrade/step_relink.go @@ -21,22 +21,22 @@ func (u *Upgrader) changeSymlink(ctx context.Context, newHash string) error { // create symlink to elastic-agent-{hash} hashedDir := fmt.Sprintf("%s-%s", agentName, newHash) - agentBakName := agentName + ".bak" - symlinkPath := filepath.Join(paths.Config(), agentName) - newPath := filepath.Join(paths.Data(), hashedDir, agentName) + agentPrevName := agentName + ".prev" + symlinkPath := filepath.Join(paths.Top(), agentName) + newPath := filepath.Join(paths.Top(), "data", hashedDir, agentName) // handle windows suffixes if runtime.GOOS == "windows" { - agentBakName = agentName + ".exe.back" //.bak is already used + agentPrevName = agentName + ".exe.prev" symlinkPath += ".exe" newPath += ".exe" } - bakNewPath := filepath.Join(paths.Config(), agentBakName) - if err := os.Symlink(newPath, bakNewPath); err != nil { + prevNewPath := filepath.Join(paths.Top(), agentPrevName) + if err := os.Symlink(newPath, prevNewPath); err != nil { return errors.New(err, errors.TypeFilesystem, "failed to update agent symlink") } // safely rotate - return file.SafeFileRotate(symlinkPath, bakNewPath) + return file.SafeFileRotate(symlinkPath, prevNewPath) } diff --git a/x-pack/elastic-agent/pkg/agent/application/upgrade/upgrade.go b/x-pack/elastic-agent/pkg/agent/application/upgrade/upgrade.go index 08c38aba8c5b..9c2c4f022869 100644 --- a/x-pack/elastic-agent/pkg/agent/application/upgrade/upgrade.go +++ b/x-pack/elastic-agent/pkg/agent/application/upgrade/upgrade.go @@ -17,6 +17,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/info" "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/agent/install" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/artifact" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/fleetapi" @@ -32,11 +33,12 @@ const ( // Upgrader performs an upgrade type Upgrader struct { - settings *artifact.Config - log *logger.Logger - closers []context.CancelFunc - reexec reexecManager - acker acker + settings *artifact.Config + log *logger.Logger + closers []context.CancelFunc + reexec reexecManager + acker acker + upgradable bool } type reexecManager interface { @@ -51,16 +53,28 @@ type acker interface { // NewUpgrader creates an upgrader which is capable of performing upgrade operation func NewUpgrader(settings *artifact.Config, log *logger.Logger, closers []context.CancelFunc, reexec reexecManager, a acker) *Upgrader { return &Upgrader{ - settings: settings, - log: log, - closers: closers, - reexec: reexec, - acker: a, + settings: settings, + log: log, + closers: closers, + reexec: reexec, + acker: a, + upgradable: getUpgradable(), } } +// Upgradable returns true if the Elastic Agent can be upgraded. +func (u *Upgrader) Upgradable() bool { + return u.upgradable +} + // Upgrade upgrades running agent func (u *Upgrader) Upgrade(ctx context.Context, a *fleetapi.ActionUpgrade) error { + if !u.upgradable { + return fmt.Errorf( + "cannot be upgraded; must be installed with install sub-command and " + + "running under control of the systems supervisor") + } + archivePath, err := u.downloadArtifact(ctx, a.Version, a.SourceURI) if err != nil { return err @@ -134,15 +148,16 @@ func (u *Upgrader) Ack(ctx context.Context) error { return ioutil.WriteFile(markerFile, markerBytes, 0600) } -func isSubdir(base, target string) (bool, error) { - relPath, err := filepath.Rel(base, target) - return strings.HasPrefix(relPath, ".."), err -} - func rollbackInstall(hash string) { os.RemoveAll(filepath.Join(paths.Data(), fmt.Sprintf("%s-%s", agentName, hash))) } +func getUpgradable() bool { + // only upgradable if running from Agent installer and running under the + // control of the system supervisor (or built specifically with upgrading enabled) + return release.Upgradable() || (install.RunningInstalled() && install.RunningUnderSupervisor()) +} + func copyActionStore(newHash string) error { currentActionStorePath := info.AgentActionStoreFile() diff --git a/x-pack/elastic-agent/pkg/agent/cmd/checks.go b/x-pack/elastic-agent/pkg/agent/cmd/checks.go deleted file mode 100644 index 4fee74970099..000000000000 --- a/x-pack/elastic-agent/pkg/agent/cmd/checks.go +++ /dev/null @@ -1,57 +0,0 @@ -// 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. - -// +build !windows - -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/spf13/cobra" - - // import logp flags - _ "github.com/elastic/beats/v7/libbeat/logp/configure" - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/paths" - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/release" -) - -// preRunCheck is noop because -// - darwin.tar - symlink created during packaging -// - linux.tar - symlink created during packaging -// - linux.rpm - symlink created using install script -// - linux.deb - symlink created using install script -// - linux.docker - symlink created using Dockerfile -func preRunCheck(flags *globalFlags) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - if sn := paths.ServiceName(); sn != "" { - // paths were created we're running as child. - return nil - } - - // get versioned path - smallHash := fmt.Sprintf("elastic-agent-%s", smallHash(release.Commit())) - commitFilepath := filepath.Join(paths.Config(), commitFile) // use other file in the future - if content, err := ioutil.ReadFile(commitFilepath); err == nil { - smallHash = hashedDirName(content) - } - - origExecPath, err := os.Executable() - if err != nil { - return err - } - reexecPath := filepath.Join(paths.Data(), smallHash, filepath.Base(origExecPath)) - - // generate paths - if err := generatePaths(filepath.Dir(reexecPath), origExecPath); err != nil { - return err - } - - paths.UpdatePaths() - return nil - } -} diff --git a/x-pack/elastic-agent/pkg/agent/cmd/checks_windows.go b/x-pack/elastic-agent/pkg/agent/cmd/checks_windows.go deleted file mode 100644 index 36108c8e08bd..000000000000 --- a/x-pack/elastic-agent/pkg/agent/cmd/checks_windows.go +++ /dev/null @@ -1,114 +0,0 @@ -// 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. - -// +build windows - -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/spf13/cobra" - - // import logp flags - _ "github.com/elastic/beats/v7/libbeat/logp/configure" - - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/paths" - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/reexec" - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/configuration" - "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" - "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/release" -) - -func preRunCheck(flags *globalFlags) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - if sn := paths.ServiceName(); sn != "" { - // replacing with correct service name so we - // can talk to service manager. - if !filepath.IsAbs(os.Args[0]) { - os.Args[0] = sn - } - - // paths were created we're running as child. - return nil - } - - smallHash := fmt.Sprintf("elastic-agent-%s", smallHash(release.Commit())) - commitFilepath := filepath.Join(paths.Config(), commitFile) - if content, err := ioutil.ReadFile(commitFilepath); err == nil { - smallHash = hashedDirName(content) - } - - // rename itself - origExecPath, err := os.Executable() - if err != nil { - return err - } - - if err := os.Rename(origExecPath, origExecPath+".bak"); err != nil { - return err - } - - // create symlink to elastic-agent-{hash} - reexecPath := filepath.Join(paths.Data(), smallHash, filepath.Base(origExecPath)) - if err := os.Symlink(reexecPath, origExecPath); err != nil { - return err - } - - // generate paths - if err := generatePaths(filepath.Dir(reexecPath), origExecPath); err != nil { - return err - } - - paths.UpdatePaths() - - // reexec if running run - if cmd.Use == "run" { - pathConfigFile := flags.Config() - rawConfig, err := config.LoadYAML(pathConfigFile) - if err != nil { - return errors.New(err, - fmt.Sprintf("could not read configuration file %s", pathConfigFile), - errors.TypeFilesystem, - errors.M(errors.MetaKeyPath, pathConfigFile)) - } - - cfg, err := configuration.NewFromConfig(rawConfig) - if err != nil { - return errors.New(err, - fmt.Sprintf("could not parse configuration file %s", pathConfigFile), - errors.TypeFilesystem, - errors.M(errors.MetaKeyPath, pathConfigFile)) - } - - logger, err := logger.NewFromConfig("", cfg.Settings.LoggingConfig) - if err != nil { - return err - } - - rexLogger := logger.Named("reexec") - rm := reexec.NewManager(rexLogger, reexecPath) - - argsOverrides := []string{ - "--path.data", paths.Data(), - "--path.home", filepath.Dir(reexecPath), - "--path.config", paths.Config(), - } - rm.ReExec(argsOverrides...) - - // trigger reexec - rm.ShutdownComplete() - - // return without running Run method - os.Exit(0) - } - - return nil - } -} diff --git a/x-pack/elastic-agent/pkg/agent/cmd/common.go b/x-pack/elastic-agent/pkg/agent/cmd/common.go index d5c195566bdb..8ca5700f3c6b 100644 --- a/x-pack/elastic-agent/pkg/agent/cmd/common.go +++ b/x-pack/elastic-agent/pkg/agent/cmd/common.go @@ -6,15 +6,10 @@ package cmd import ( "flag" - "fmt" - "io/ioutil" "os" "path/filepath" - "runtime" - "strings" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" // import logp flags _ "github.com/elastic/beats/v7/libbeat/logp/configure" @@ -58,7 +53,6 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { // path flags cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.home")) cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.config")) - cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.data")) cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.logs")) cmd.PersistentFlags().StringVarP(&flags.PathConfigFile, "c", "c", defaultConfig, `Configuration file, relative to path.config`) @@ -72,6 +66,8 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { run := newRunCommandWithArgs(flags, args, streams) cmd.AddCommand(basecmd.NewDefaultCommandsWithArgs(args, streams)...) cmd.AddCommand(run) + cmd.AddCommand(newInstallCommandWithArgs(flags, args, streams)) + cmd.AddCommand(newUninstallCommandWithArgs(flags, args, streams)) cmd.AddCommand(newEnrollCommandWithArgs(flags, args, streams)) cmd.AddCommand(newInspectCommandWithArgs(flags, args, streams)) @@ -80,58 +76,7 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { if reexec != nil { cmd.AddCommand(reexec) } - cmd.PersistentPreRunE = preRunCheck(flags) cmd.Run = run.Run return cmd } - -func hashedDirName(filecontent []byte) string { - s := strings.TrimSpace(string(filecontent)) - if len(s) == 0 { - return "elastic-agent" - } - - s = smallHash(s) - - return fmt.Sprintf("elastic-agent-%s", s) -} - -func smallHash(hash string) string { - if len(hash) > hashLen { - hash = hash[:hashLen] - } - - return hash -} - -func generatePaths(dir, origExec string) error { - pathsCfg := map[string]interface{}{ - "path.data": paths.Data(), - "path.home": dir, - "path.config": paths.Config(), - "path.service_name": origExec, - } - - pathsCfgPath := filepath.Join(paths.Data(), "paths.yml") - pathsContent, err := yaml.Marshal(pathsCfg) - if err != nil { - return err - } - - if err := ioutil.WriteFile(pathsCfgPath, pathsContent, 0740); err != nil { - return err - } - - if runtime.GOOS == "windows" { - // due to two binaries we need to do a path dance - // as versioned binary will look for path inside it's own directory - versionedPath := filepath.Join(dir, "data", "paths.yml") - if err := os.MkdirAll(filepath.Dir(versionedPath), 0700); err != nil { - return err - } - return os.Symlink(pathsCfgPath, versionedPath) - } - - return nil -} diff --git a/x-pack/elastic-agent/pkg/agent/cmd/enroll.go b/x-pack/elastic-agent/pkg/agent/cmd/enroll.go index 6749b57b2506..6a604554136a 100644 --- a/x-pack/elastic-agent/pkg/agent/cmd/enroll.go +++ b/x-pack/elastic-agent/pkg/agent/cmd/enroll.go @@ -42,18 +42,55 @@ func newEnrollCommandWithArgs(flags *globalFlags, _ []string, streams *cli.IOStr }, } + addEnrollFlags(cmd) + cmd.Flags().BoolP("force", "f", false, "Force overwrite the current and do not prompt for confirmation") + cmd.Flags().Bool("no-restart", false, "Skip restarting the currently running daemon") + + // used by install command + cmd.Flags().BoolP("from-install", "", false, "Set by install command to signal this was executed from install") + cmd.Flags().MarkHidden("from-install") + + return cmd +} + +func addEnrollFlags(cmd *cobra.Command) { cmd.Flags().StringP("certificate-authorities", "a", "", "Comma separated list of root certificate for server verifications") cmd.Flags().StringP("ca-sha256", "p", "", "Comma separated list of certificate authorities hash pins used for certificate verifications") - cmd.Flags().BoolP("force", "f", false, "Force overwrite the current and do not prompt for confirmation") cmd.Flags().BoolP("insecure", "i", false, "Allow insecure connection to Kibana") cmd.Flags().StringP("staging", "", "", "Configures agent to download artifacts from a staging build") - cmd.Flags().Bool("no-restart", false, "Skip restarting the currently running daemon") +} - return cmd +func buildEnrollmentFlags(cmd *cobra.Command) []string { + ca, _ := cmd.Flags().GetString("certificate-authorities") + sha256, _ := cmd.Flags().GetString("ca-sha256") + insecure, _ := cmd.Flags().GetBool("insecure") + staging, _ := cmd.Flags().GetString("staging") + + args := []string{} + if ca != "" { + args = append(args, "--certificate-authorities") + args = append(args, ca) + } + if sha256 != "" { + args = append(args, "--ca-sha256") + args = append(args, sha256) + } + if insecure { + args = append(args, "--insecure") + } + if staging != "" { + args = append(args, "--staging") + args = append(args, staging) + } + return args } func enroll(streams *cli.IOStreams, cmd *cobra.Command, flags *globalFlags, args []string) error { - warn.PrintNotGA(streams.Out) + fromInstall, _ := cmd.Flags().GetBool("from-install") + if !fromInstall { + warn.PrintNotGA(streams.Out) + } + pathConfigFile := flags.Config() rawConfig, err := application.LoadConfigFromFile(pathConfigFile) if err != nil { @@ -79,13 +116,16 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command, flags *globalFlags, args } force, _ := cmd.Flags().GetBool("force") + if fromInstall { + force = true + } if !force { confirm, err := c.Confirm("This will replace your current settings. Do you want to continue?", true) if err != nil { return errors.New(err, "problem reading prompt response") } if !confirm { - fmt.Fprintln(streams.Out, "Enrollment was canceled by the user") + fmt.Fprintln(streams.Out, "Enrollment was cancelled by the user") return nil } } diff --git a/x-pack/elastic-agent/pkg/agent/cmd/install.go b/x-pack/elastic-agent/pkg/agent/cmd/install.go new file mode 100644 index 000000000000..177f79912c98 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/cmd/install.go @@ -0,0 +1,154 @@ +// 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 cmd + +import ( + "fmt" + "os" + "os/exec" + + "github.com/spf13/cobra" + + c "github.com/elastic/beats/v7/libbeat/common/cli" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/install" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/warn" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/cli" +) + +func newInstallCommandWithArgs(flags *globalFlags, _ []string, streams *cli.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "install", + Short: "Install Elastic Agent permanently on this system", + Long: `This will install Elastic Agent permanently on this system and will become managed by the systems service manager. + +Unless all the require command-line parameters are provided or -f is used this command will ask questions on how you +would like the Agent to operate. +`, + Run: func(c *cobra.Command, args []string) { + if err := installCmd(streams, c, flags, args); err != nil { + fmt.Fprintf(streams.Err, "%v\n", err) + os.Exit(1) + } + }, + } + + cmd.Flags().StringP("kibana-url", "k", "", "URL of Kibana to enroll Agent into Fleet") + cmd.Flags().StringP("enrollment-token", "t", "", "Enrollment token to use to enroll Agent into Fleet") + cmd.Flags().BoolP("force", "f", false, "Force overwrite the current and do not prompt for confirmation") + addEnrollFlags(cmd) + + return cmd +} + +func installCmd(streams *cli.IOStreams, cmd *cobra.Command, flags *globalFlags, args []string) error { + if !install.HasRoot() { + return fmt.Errorf("unable to perform install command, not executed with %s permissions", install.PermissionUser) + } + status, reason := install.Status() + if status == install.Installed { + return fmt.Errorf("already installed at: %s", install.InstallPath) + } + + warn.PrintNotGA(streams.Out) + force, _ := cmd.Flags().GetBool("force") + if status == install.Broken { + if !force { + fmt.Fprintf(streams.Out, "Elastic Agent is installed but currently broken: %s\n", reason) + confirm, err := c.Confirm(fmt.Sprintf("Continuing will re-install Elastic Agent over the current installation at %s. Do you want to continue?", install.InstallPath), true) + if err != nil { + return fmt.Errorf("Error: problem reading prompt response") + } + if !confirm { + return fmt.Errorf("installation was cancelled by the user") + } + } + } else { + if !force { + confirm, err := c.Confirm(fmt.Sprintf("Elastic Agent will be installed at %s and will run as a service. Do you want to continue?", install.InstallPath), true) + if err != nil { + return fmt.Errorf("Error: problem reading prompt response") + } + if !confirm { + return fmt.Errorf("installation was cancelled by the user") + } + } + } + + err := install.Install() + if err != nil { + return fmt.Errorf("Error: %s", err) + } + err = install.StartService() + if err != nil { + fmt.Fprintf(streams.Out, "Installation of required system files was successful, but starting of the service failed.\n") + return err + } + fmt.Fprintf(streams.Out, "Installation was successful and Elastic Agent is running.\n") + + askEnroll := true + kibana, _ := cmd.Flags().GetString("kibana-url") + token, _ := cmd.Flags().GetString("enrollment-token") + if kibana != "" && token != "" { + askEnroll = false + } + if force { + askEnroll = false + } + if askEnroll { + confirm, err := c.Confirm("Do you want to enroll this Agent into Fleet?", true) + if err != nil { + return fmt.Errorf("problem reading prompt response") + } + if !confirm { + // not enrolling, all done (standalone mode) + return nil + } + } + if !askEnroll && (kibana == "" || token == "") { + // force was performed without required enrollment arguments, all done (standalone mode) + return nil + } + + if kibana == "" { + kibana, err = c.ReadInput("Kibana URL you want to enroll this Agent into:") + if err != nil { + return fmt.Errorf("problem reading prompt response") + } + if kibana == "" { + fmt.Fprintf(streams.Out, "Enrollment cancelled because no URL was provided.\n") + return nil + } + } + if token == "" { + token, err = c.ReadInput("Fleet enrollment token:") + if err != nil { + return fmt.Errorf("problem reading prompt response") + } + if token == "" { + fmt.Fprintf(streams.Out, "Enrollment cancelled because no enrollment token was provided.\n") + return nil + } + } + + enrollArgs := []string{"enroll", kibana, token, "--from-install"} + enrollArgs = append(enrollArgs, buildEnrollmentFlags(cmd)...) + enrollCmd := exec.Command(install.ExecutablePath(), enrollArgs...) + enrollCmd.Stdin = os.Stdin + enrollCmd.Stdout = os.Stdout + enrollCmd.Stderr = os.Stderr + err = enrollCmd.Start() + if err != nil { + return fmt.Errorf("failed to execute enroll command: %s", err) + } + err = enrollCmd.Wait() + if err == nil { + return nil + } + exitErr, ok := err.(*exec.ExitError) + if ok { + return fmt.Errorf("enroll command failed with exit code: %d", exitErr.ExitCode()) + } + return fmt.Errorf("enroll command failed for unknown reason: %s", err) +} diff --git a/x-pack/elastic-agent/pkg/agent/cmd/uninstall.go b/x-pack/elastic-agent/pkg/agent/cmd/uninstall.go new file mode 100644 index 000000000000..d215a15e3371 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/cmd/uninstall.go @@ -0,0 +1,95 @@ +// 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 cmd + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/spf13/cobra" + + c "github.com/elastic/beats/v7/libbeat/common/cli" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/install" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/cli" +) + +func newUninstallCommandWithArgs(flags *globalFlags, _ []string, streams *cli.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "uninstall", + Short: "Uninstall permanent Elastic Agent from this system", + Long: `This will uninstall permanent Elastic Agent from this system and will no longer be managed by this system. + +Unless -f is used this command will ask confirmation before performing removal. +`, + Run: func(c *cobra.Command, args []string) { + if err := uninstallCmd(streams, c, flags, args); err != nil { + fmt.Fprintf(streams.Err, "%v\n", err) + os.Exit(1) + } + }, + } + + cmd.Flags().BoolP("force", "f", false, "Force overwrite the current and do not prompt for confirmation") + + return cmd +} + +func uninstallCmd(streams *cli.IOStreams, cmd *cobra.Command, flags *globalFlags, args []string) error { + if !install.HasRoot() { + return fmt.Errorf("unable to perform uninstall command, not executed with %s permissions", install.PermissionUser) + } + status, reason := install.Status() + if status == install.NotInstalled { + return fmt.Errorf("not installed") + } + if status == install.Installed && !install.RunningInstalled() { + return fmt.Errorf("can only be uninstall by executing the installed Elastic Agent at: %s", install.ExecutablePath()) + } + + force, _ := cmd.Flags().GetBool("force") + if status == install.Broken { + if !force { + fmt.Fprintf(streams.Out, "Elastic Agent is installed but currently broken: %s\n", reason) + confirm, err := c.Confirm(fmt.Sprintf("Continuing will uninstall the broken Elastic Agent at %s. Do you want to continue?", install.InstallPath), true) + if err != nil { + return fmt.Errorf("problem reading prompt response") + } + if !confirm { + return fmt.Errorf("uninstall was cancelled by the user") + } + } + } else { + if !force { + confirm, err := c.Confirm(fmt.Sprintf("Elastic Agent will be uninstalled from your system at %s. Do you want to continue?", install.InstallPath), true) + if err != nil { + return fmt.Errorf("problem reading prompt response") + } + if !confirm { + return fmt.Errorf("uninstall was cancelled by the user") + } + } + } + + err := install.Uninstall() + if err != nil { + return err + } + fmt.Fprintf(streams.Out, "Elastic Agent has been uninstalled.\n") + + if runtime.GOOS == "windows" { + // The installation path will still exists because we are executing from that + // directory. So cmd.exe is spawned that sleeps for 2 seconds (using ping, recommend way from + // from Windows) then rmdir is performed. + rmdir := exec.Command( + filepath.Join(os.Getenv("windir"), "system32", "cmd.exe"), + "/C", "ping", "-n", "2", "127.0.0.1", "&&", "rmdir", "/s", "/q", install.InstallPath) + _ = rmdir.Start() + } + + return nil +} diff --git a/x-pack/elastic-agent/pkg/agent/control/addr.go b/x-pack/elastic-agent/pkg/agent/control/addr.go index 20bc1e6a0056..31005e8e34d8 100644 --- a/x-pack/elastic-agent/pkg/agent/control/addr.go +++ b/x-pack/elastic-agent/pkg/agent/control/addr.go @@ -11,10 +11,17 @@ import ( "fmt" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/paths" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/install" ) // Address returns the address to connect to Elastic Agent daemon. func Address() string { + // when installed the control address is fixed + if install.RunningInstalled() { + return install.SocketPath + } + + // not install, adjust the path based on data path data := paths.Data() // entire string cannot be longer than 107 characters, this forces the // length to always be 88 characters (but unique per data path) diff --git a/x-pack/elastic-agent/pkg/agent/control/addr_windows.go b/x-pack/elastic-agent/pkg/agent/control/addr_windows.go index bf2e164fbaec..cbfcdf2c99ea 100644 --- a/x-pack/elastic-agent/pkg/agent/control/addr_windows.go +++ b/x-pack/elastic-agent/pkg/agent/control/addr_windows.go @@ -11,10 +11,17 @@ import ( "fmt" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/paths" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/install" ) // Address returns the address to connect to Elastic Agent daemon. func Address() string { + // when installed the control address is fixed + if install.RunningInstalled() { + return install.SocketPath + } + + // not install, adjust the path based on data path data := paths.Data() // entire string cannot be longer than 256 characters, this forces the // length to always be 87 characters (but unique per data path) diff --git a/x-pack/elastic-agent/pkg/agent/control/server/listener.go b/x-pack/elastic-agent/pkg/agent/control/server/listener.go index 7edfc7b8ee97..3090f3d140d8 100644 --- a/x-pack/elastic-agent/pkg/agent/control/server/listener.go +++ b/x-pack/elastic-agent/pkg/agent/control/server/listener.go @@ -8,7 +8,6 @@ package server import ( "fmt" - "net" "os" "path/filepath" diff --git a/x-pack/elastic-agent/pkg/agent/control/server/listener_windows.go b/x-pack/elastic-agent/pkg/agent/control/server/listener_windows.go index f98c32bcee3d..eaedc9f88f21 100644 --- a/x-pack/elastic-agent/pkg/agent/control/server/listener_windows.go +++ b/x-pack/elastic-agent/pkg/agent/control/server/listener_windows.go @@ -10,6 +10,8 @@ import ( "net" "os/user" + "github.com/pkg/errors" + "github.com/elastic/beats/v7/libbeat/api/npipe" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/control" @@ -18,11 +20,7 @@ import ( // createListener creates a named pipe listener on Windows func createListener(_ *logger.Logger) (net.Listener, error) { - u, err := user.Current() - if err != nil { - return nil, err - } - sd, err := npipe.DefaultSD(u.Username) + sd, err := securityDescriptor() if err != nil { return nil, err } @@ -32,3 +30,26 @@ func createListener(_ *logger.Logger) (net.Listener, error) { func cleanupListener(_ *logger.Logger) { // nothing to do on windows } + +func securityDescriptor() (string, error) { + u, err := user.Current() + if err != nil { + return "", errors.Wrap(err, "failed to get current user") + } + // Named pipe security and access rights. + // We create the pipe and the specific users should only be able to write to it. + // See docs: https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-security-and-access-rights + // String definition: https://docs.microsoft.com/en-us/windows/win32/secauthz/ace-strings + // Give generic read/write access to the specified user. + descriptor := "D:P(A;;GA;;;" + u.Uid + ")" + if u.Username == "NT AUTHORITY\\SYSTEM" { + // running as SYSTEM, include Administrators group so Administrators can talk over + // the named pipe to the running Elastic Agent system process + admin, err := user.LookupGroup("Administrators") + if err != nil { + return "", errors.Wrap(err, "failed to lookup Administrators group") + } + descriptor += "(A;;GA;;;" + admin.Gid + ")" + } + return descriptor, nil +} diff --git a/x-pack/elastic-agent/pkg/agent/install/install.go b/x-pack/elastic-agent/pkg/agent/install/install.go new file mode 100644 index 000000000000..2705ea0bfd93 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/install.go @@ -0,0 +1,142 @@ +// 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 install + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/otiai10/copy" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/release" +) + +// Install installs Elastic Agent persistently on the system including creating and starting its service. +func Install() error { + dir, err := findDirectory() + if err != nil { + return errors.New(err, "failed to discover the source directory for installation", errors.TypeFilesystem) + } + + // uninstall current installation + err = Uninstall() + if err != nil { + return err + } + + // ensure parent directory exists, copy source into install path + err = os.MkdirAll(filepath.Dir(InstallPath), 0755) + if err != nil { + return errors.New( + err, + fmt.Sprintf("failed to create installation parent directory (%s)", filepath.Dir(InstallPath)), + errors.M("directory", filepath.Dir(InstallPath))) + } + err = copy.Copy(dir, InstallPath, copy.Options{ + OnSymlink: func(_ string) copy.SymlinkAction { + return copy.Shallow + }, + Sync: true, + }) + if err != nil { + return errors.New( + err, + fmt.Sprintf("failed to copy source directory (%s) to destination (%s)", dir, InstallPath), + errors.M("source", dir), errors.M("destination", InstallPath)) + } + + // place shell wrapper, if present on platform + if ShellWrapperPath != "" { + err = ioutil.WriteFile(ShellWrapperPath, []byte(ShellWrapper), 0755) + if err != nil { + return errors.New( + err, + fmt.Sprintf("failed to write shell wrapper (%s)", ShellWrapperPath), + errors.M("destination", ShellWrapperPath)) + } + } + + // post install (per platform) + err = postInstall() + if err != nil { + return err + } + + // install service + svc, err := newService() + if err != nil { + return err + } + err = svc.Install() + if err != nil { + return errors.New( + err, + fmt.Sprintf("failed to install service (%s)", ServiceName), + errors.M("service", ServiceName)) + } + return nil +} + +// StartService starts the installed service. +// +// This should only be called after Install is successful. +func StartService() error { + svc, err := newService() + if err != nil { + return err + } + err = svc.Start() + if err != nil { + return errors.New( + err, + fmt.Sprintf("failed to start service (%s)", ServiceName), + errors.M("service", ServiceName)) + } + return nil +} + +// findDirectory returns the directory to copy into the installation location. +// +// This also verifies that the discovered directory is a valid directory for installation. +func findDirectory() (string, error) { + execPath, err := os.Executable() + if err != nil { + return "", err + } + execPath, err = filepath.Abs(execPath) + if err != nil { + return "", err + } + sourceDir := filepath.Dir(execPath) + if insideData(sourceDir) { + // executable path is being reported as being down inside of data path + // move up to directories to perform the copy + sourceDir = filepath.Dir(filepath.Dir(sourceDir)) + } + err = verifyDirectory(sourceDir) + if err != nil { + return "", err + } + return sourceDir, nil +} + +// verifyDirectory ensures that the directory includes the executable. +func verifyDirectory(dir string) error { + _, err := os.Stat(filepath.Join(dir, BinaryName)) + if os.IsNotExist(err) { + return fmt.Errorf("missing %s", BinaryName) + } + return nil +} + +// insideData returns true when the exePath is inside of the current Agents data path. +func insideData(exePath string) bool { + expectedPath := filepath.Join("data", fmt.Sprintf("elastic-agent-%s", release.ShortCommit())) + return strings.HasSuffix(exePath, expectedPath) +} diff --git a/x-pack/elastic-agent/pkg/agent/install/install_unix.go b/x-pack/elastic-agent/pkg/agent/install/install_unix.go new file mode 100644 index 000000000000..07d696d580a1 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/install_unix.go @@ -0,0 +1,13 @@ +// 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. + +// +build !windows + +package install + +// postInstall performs post installation for unix-based systems. +func postInstall() error { + // do nothing + return nil +} diff --git a/x-pack/elastic-agent/pkg/agent/install/install_windows.go b/x-pack/elastic-agent/pkg/agent/install/install_windows.go new file mode 100644 index 000000000000..ec10467ce79a --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/install_windows.go @@ -0,0 +1,35 @@ +// 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. + +// +build windows + +package install + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/release" +) + +// postInstall performs post installation for Windows systems. +func postInstall() error { + // delete the top-level elastic-agent.exe + binary := filepath.Join(InstallPath, BinaryName) + err := os.Remove(binary) + if err != nil { + // do not handle does not exist, it should have existed + return err + } + + // create top-level symlink to nested binary + realBinary := filepath.Join(InstallPath, "data", fmt.Sprintf("elastic-agent-%s", release.ShortCommit()), BinaryName) + err = os.Symlink(realBinary, binary) + if err != nil { + return err + } + + return nil +} diff --git a/x-pack/elastic-agent/pkg/agent/install/installed.go b/x-pack/elastic-agent/pkg/agent/install/installed.go new file mode 100644 index 000000000000..9f294078242d --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/installed.go @@ -0,0 +1,75 @@ +// 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 install + +import ( + "os" + "path/filepath" + + "github.com/kardianos/service" +) + +// StatusType is the return status types. +type StatusType int + +const ( + // NotInstalled returned when Elastic Agent is not installed. + NotInstalled StatusType = iota + // Installed returned when Elastic Agent is installed currectly. + Installed + // Broken returned when Elastic Agent is installed but broken. + Broken +) + +// Status returns the installation status of Agent. +func Status() (StatusType, string) { + expected := filepath.Join(InstallPath, BinaryName) + status, reason := checkService() + _, err := os.Stat(expected) + if os.IsNotExist(err) { + if status == Installed { + // service installed, but no install path + return Broken, "service exists but installation path is missing" + } + return NotInstalled, "no install path or service" + } + if status == NotInstalled { + // install path present, but not service + return Broken, reason + } + return Installed, "" +} + +// RunningInstalled returns true when executing Agent is the installed Agent. +// +// This verifies the running executable path based on hard-coded paths +// for each platform type. +func RunningInstalled() bool { + expected := filepath.Join(InstallPath, BinaryName) + execPath, _ := os.Executable() + execPath, _ = filepath.Abs(execPath) + execName := filepath.Base(execPath) + execDir := filepath.Dir(execPath) + if insideData(execDir) { + // executable path is being reported as being down inside of data path + // move up to directories to perform the comparison + execDir = filepath.Dir(filepath.Dir(execDir)) + execPath = filepath.Join(execDir, execName) + } + return expected == execPath +} + +// checkService only checks the status of the service. +func checkService() (StatusType, string) { + svc, err := newService() + if err != nil { + return NotInstalled, "unable to check service status" + } + status, _ := svc.Status() + if status == service.StatusUnknown { + return NotInstalled, "service is not installed" + } + return Installed, "" +} diff --git a/x-pack/elastic-agent/pkg/agent/install/paths.go b/x-pack/elastic-agent/pkg/agent/install/paths.go new file mode 100644 index 000000000000..5936c31926ab --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/paths.go @@ -0,0 +1,30 @@ +// 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. + +// +build !darwin +// +build !windows + +package install + +const ( + // BinaryName is the name of the installed binary. + BinaryName = "elastic-agent" + + // InstallPath is the installation path using for install command. + InstallPath = "/opt/Elastic/Agent" + + // SocketPath is the socket path used when installed. + SocketPath = "unix:///run/elastic-agent.sock" + + // ServiceName is the service name when installed. + ServiceName = "elastic-agent" + + // ShellWrapperPath is the path to the installed shell wrapper. + ShellWrapperPath = "/usr/bin/elastic-agent" + + // ShellWrapper is the wrapper that is installed. + ShellWrapper = `#!/bin/sh +exec /opt/Elastic/Agent/elastic-agent $@ +` +) diff --git a/x-pack/elastic-agent/pkg/agent/install/paths_darwin.go b/x-pack/elastic-agent/pkg/agent/install/paths_darwin.go new file mode 100644 index 000000000000..f6ebcdfdbae9 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/paths_darwin.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. + +// +build darwin + +package install + +const ( + // BinaryName is the name of the installed binary. + BinaryName = "elastic-agent" + + // InstallPath is the installation path using for install command. + InstallPath = "/Library/Elastic/Agent" + + // SocketPath is the socket path used when installed. + SocketPath = "unix:///var/run/elastic-agent.sock" + + // ServiceName is the service name when installed. + ServiceName = "co.elastic.elastic-agent" + + // ShellWrapperPath is the path to the installed shell wrapper. + ShellWrapperPath = "/usr/local/bin/elastic-agent" + + // ShellWrapper is the wrapper that is installed. + ShellWrapper = `#!/bin/sh +exec /Library/Elastic/Agent/elastic-agent $@ +` +) diff --git a/x-pack/elastic-agent/pkg/agent/install/paths_windows.go b/x-pack/elastic-agent/pkg/agent/install/paths_windows.go new file mode 100644 index 000000000000..3af4c79d9415 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/paths_windows.go @@ -0,0 +1,27 @@ +// 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. + +// +build windows + +package install + +const ( + // BinaryName is the name of the installed binary. + BinaryName = "elastic-agent.exe" + + // InstallPath is the installation path using for install command. + InstallPath = `C:\Program Files\Elastic\Agent` + + // SocketPath is the socket path used when installed. + SocketPath = `\\.\pipe\elastic-agent-system` + + // ServiceName is the service name when installed. + ServiceName = "Elastic Agent" + + // ShellWrapperPath is the path to the installed shell wrapper. + ShellWrapperPath = "" // no wrapper on Windows + + // ShellWrapper is the wrapper that is installed. + ShellWrapper = "" // no wrapper on Windows +) diff --git a/x-pack/elastic-agent/pkg/agent/install/root_unix.go b/x-pack/elastic-agent/pkg/agent/install/root_unix.go new file mode 100644 index 000000000000..b79f104d98d9 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/root_unix.go @@ -0,0 +1,19 @@ +// 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. + +// +build !windows + +package install + +import "os" + +const ( + // PermissionUser is the permission level the user needs to be. + PermissionUser = "root" +) + +// HasRoot returns true if the user has root permissions. +func HasRoot() bool { + return os.Getegid() == 0 +} diff --git a/x-pack/elastic-agent/pkg/agent/install/root_windows.go b/x-pack/elastic-agent/pkg/agent/install/root_windows.go new file mode 100644 index 000000000000..c72432084ace --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/root_windows.go @@ -0,0 +1,27 @@ +// 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. + +// +build windows + +package install + +import ( + "os" +) + +const ( + // PermissionUser is the permission level the user needs to be. + PermissionUser = "Administrator" +) + +// HasRoot returns true if the user has Administrator/SYSTEM permissions. +func HasRoot() bool { + // only valid rights can open the physical drive + f, err := os.Open("\\\\.\\PHYSICALDRIVE0") + if err != nil { + return false + } + defer f.Close() + return true +} diff --git a/x-pack/elastic-agent/pkg/agent/install/svc.go b/x-pack/elastic-agent/pkg/agent/install/svc.go new file mode 100644 index 000000000000..18cbc6d840f5 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/svc.go @@ -0,0 +1,38 @@ +// 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 install + +import ( + "path/filepath" + + "github.com/kardianos/service" +) + +const ( + // ServiceDisplayName is the service display name for the service. + ServiceDisplayName = "Elastic Agent" + + // ServiceDescription is the description for the service. + ServiceDescription = "Elastic Agent is a unified agent to observe, monitor and protect your system." +) + +// ExecutablePath returns the path for the installed Agents executable. +func ExecutablePath() string { + exec := filepath.Join(InstallPath, BinaryName) + if ShellWrapperPath != "" { + exec = ShellWrapperPath + } + return exec +} + +func newService() (service.Service, error) { + return service.New(nil, &service.Config{ + Name: ServiceName, + DisplayName: ServiceDisplayName, + Description: ServiceDescription, + Executable: ExecutablePath(), + WorkingDirectory: InstallPath, + }) +} diff --git a/x-pack/elastic-agent/pkg/agent/install/svc_unix.go b/x-pack/elastic-agent/pkg/agent/install/svc_unix.go new file mode 100644 index 000000000000..c7acb998489f --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/svc_unix.go @@ -0,0 +1,15 @@ +// 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. + +// +build !windows + +package install + +import "os" + +// RunningUnderSupervisor returns true when executing Agent is running under +// the supervisor processes of the OS. +func RunningUnderSupervisor() bool { + return os.Getppid() == 1 +} diff --git a/x-pack/elastic-agent/pkg/agent/install/svc_windows.go b/x-pack/elastic-agent/pkg/agent/install/svc_windows.go new file mode 100644 index 000000000000..9084f3b5ea70 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/svc_windows.go @@ -0,0 +1,49 @@ +// 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. + +// +build windows + +package install + +import ( + "golang.org/x/sys/windows" +) + +// RunningUnderSupervisor returns true when executing Agent is running under +// the supervisor processes of the OS. +func RunningUnderSupervisor() bool { + serviceSid, err := allocSid(windows.SECURITY_SERVICE_RID) + if err != nil { + return false + } + defer windows.FreeSid(serviceSid) + + t, err := windows.OpenCurrentProcessToken() + if err != nil { + return false + } + defer t.Close() + + gs, err := t.GetTokenGroups() + if err != nil { + return false + } + + for _, g := range gs.AllGroups() { + if windows.EqualSid(g.Sid, serviceSid) { + return true + } + } + return false +} + +func allocSid(subAuth0 uint32) (*windows.SID, error) { + var sid *windows.SID + err := windows.AllocateAndInitializeSid(&windows.SECURITY_NT_AUTHORITY, + 1, subAuth0, 0, 0, 0, 0, 0, 0, 0, &sid) + if err != nil { + return nil, err + } + return sid, nil +} diff --git a/x-pack/elastic-agent/pkg/agent/install/uninstall.go b/x-pack/elastic-agent/pkg/agent/install/uninstall.go new file mode 100644 index 000000000000..381427eb8c72 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/install/uninstall.go @@ -0,0 +1,71 @@ +// 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 install + +import ( + "fmt" + "os" + "runtime" + + "github.com/kardianos/service" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" +) + +// Uninstall uninstalls persistently Elastic Agent on the system. +func Uninstall() error { + // uninstall the current service + svc, err := newService() + if err != nil { + return err + } + status, _ := svc.Status() + if status == service.StatusRunning { + err := svc.Stop() + if err != nil { + return errors.New( + err, + fmt.Sprintf("failed to stop service (%s)", ServiceName), + errors.M("service", ServiceName)) + } + status = service.StatusStopped + } + if status == service.StatusStopped { + err := svc.Uninstall() + if err != nil { + return errors.New( + err, + fmt.Sprintf("failed to uninstall service (%s)", ServiceName), + errors.M("service", ServiceName)) + } + } + + // remove, if present on platform + if ShellWrapperPath != "" { + err = os.Remove(ShellWrapperPath) + if !os.IsNotExist(err) && err != nil { + return errors.New( + err, + fmt.Sprintf("failed to remove shell wrapper (%s)", ShellWrapperPath), + errors.M("destination", ShellWrapperPath)) + } + } + + // remove existing directory + err = os.RemoveAll(InstallPath) + if err != nil { + if runtime.GOOS == "windows" { + // possible to fail on Windows, because elastic-agent.exe is running from + // this directory. + return nil + } + return errors.New( + err, + fmt.Sprintf("failed to remove installation directory (%s)", InstallPath), + errors.M("directory", InstallPath)) + } + + return nil +} diff --git a/x-pack/elastic-agent/pkg/agent/warn/warn.go b/x-pack/elastic-agent/pkg/agent/warn/warn.go index 03d746992f6e..182d2af50b01 100644 --- a/x-pack/elastic-agent/pkg/agent/warn/warn.go +++ b/x-pack/elastic-agent/pkg/agent/warn/warn.go @@ -21,4 +21,5 @@ func LogNotGA(log *logger.Logger) { // PrintNotGA writes to the received writer that the Agent is not GA. func PrintNotGA(output io.Writer) { fmt.Fprintln(output, message) + fmt.Fprintln(output) } diff --git a/x-pack/elastic-agent/pkg/release/upgrade.go b/x-pack/elastic-agent/pkg/release/upgrade.go new file mode 100644 index 000000000000..ac1e8552dd25 --- /dev/null +++ b/x-pack/elastic-agent/pkg/release/upgrade.go @@ -0,0 +1,10 @@ +// 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 release + +// Upgradable return true when release is built specifically for upgrading. +func Upgradable() bool { + return allowUpgrade == "true" +} diff --git a/x-pack/elastic-agent/pkg/release/version.go b/x-pack/elastic-agent/pkg/release/version.go index 37579ac86de5..dae609a67334 100644 --- a/x-pack/elastic-agent/pkg/release/version.go +++ b/x-pack/elastic-agent/pkg/release/version.go @@ -12,6 +12,10 @@ import ( libbeatVersion "github.com/elastic/beats/v7/libbeat/version" ) +const ( + hashLen = 6 +) + // snapshot is a flag marking build as a snapshot. var snapshot = "" @@ -19,11 +23,24 @@ var snapshot = "" // without valid pgp var allowEmptyPgp string +// allowUpgrade is used as a debug flag and allows working +// with upgrade without requiring Agent to be installed correctly +var allowUpgrade string + // Commit returns the current build hash or unknown if it was not injected in the build process. func Commit() string { return libbeatVersion.Commit() } +// ShortCommit returns commit up to 6 characters. +func ShortCommit() string { + hash := Commit() + if len(hash) > hashLen { + hash = hash[:hashLen] + } + return hash +} + // BuildTime returns the build time of the binaries. func BuildTime() time.Time { return libbeatVersion.BuildTime()