internal/pkg/agent/application/paths/common.go (230 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package paths
import (
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/elastic/elastic-agent/internal/pkg/release"
"github.com/elastic/elastic-agent/pkg/utils"
// this is not a leftover: this anonymous import is needed for version initialization
_ "github.com/elastic/elastic-agent/version"
)
const (
// DefaultConfigName is the default name of the configuration file.
DefaultConfigName = "elastic-agent.yml"
// DefaultOtelConfigName is the default name of the otel configuration file.
DefaultOtelConfigName = "otel.yml"
// AgentLockFileName is the name of the overall Elastic Agent file lock.
AgentLockFileName = "agent.lock"
// ControlSocketName is the control socket name.
ControlSocketName = "elastic-agent.sock"
// WindowsControlSocketInstalledPath is the control socket path used when installed on Windows.
WindowsControlSocketInstalledPath = `npipe:///elastic-agent-system`
// MarkerFileName is the name of the file that's created by
// `elastic-agent install` in the Agent's topPath folder to
// indicate that the Agent executing from the binary under
// the same topPath folder is an installed Agent.
MarkerFileName = ".installed"
tempSubdir = "tmp"
tempSubdirPerms = 0o770
darwin = "darwin"
)
// ExternalInputsPattern is a glob that matches the paths of external configuration files.
var ExternalInputsPattern = filepath.Join("inputs.d", "*.yml")
var (
topPath string
configPath string
configFilePath string
logsPath string
downloadsPath string
componentsPath string
installPath string
controlSocketPath string
unversionedHome bool
tmpCreator sync.Once
)
func init() {
// this is the first call where we need version information (it calls isInsideData())
topPath = initialTop()
configPath = topPath
logsPath = topPath
controlSocketPath = initialControlSocketPath(topPath)
unversionedHome = false // only versioned by container subcommand
// these should never change
versionedHome := VersionedHome(topPath)
downloadsPath = filepath.Join(versionedHome, "downloads")
componentsPath = filepath.Join(versionedHome, "components")
fs := flag.CommandLine
fs.StringVar(&topPath, "path.home", topPath, "Agent root path")
fs.BoolVar(&unversionedHome, "path.home.unversioned", unversionedHome, "Agent root path is not versioned based on build")
fs.StringVar(&configPath, "path.config", configPath, "Config path is the directory Agent looks for its config file")
fs.StringVar(&configFilePath, "config", DefaultConfigName, "Configuration file, relative to path.config")
fs.StringVar(&configFilePath, "c", DefaultConfigName, "Configuration file, relative to path.config")
fs.StringVar(&logsPath, "path.logs", logsPath, "Logs path contains Agent log output")
fs.StringVar(&controlSocketPath, "path.socket", controlSocketPath, "Control protocol socket path for the Agent")
// enable user to download update artifacts to alternative place
// TODO: remove path.downloads support on next major (this can be configured using `agent.download.targetDirectory`)
// `path.download` serves just as init value for `agent.download.targetDirectory`
fs.StringVar(&downloadsPath, "path.downloads", downloadsPath, "Downloads path contains binaries Agent downloads")
}
// 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
}
// SetTop overrides the Top path.
//
// Used by the container subcommand to adjust the overall top path allowing state can be maintained between container
// restarts.
func SetTop(path string) {
topPath = path
}
// TempDir returns agent temp dir located within data dir.
func TempDir() string {
tmpDir := filepath.Join(Data(), tempSubdir)
tmpCreator.Do(func() {
// create tempdir as it probably don't exists
_ = os.MkdirAll(tmpDir, tempSubdirPerms)
})
return tmpDir
}
// Home returns a directory where binary lives
func Home() string {
return HomeFrom(topPath)
}
func HomeFrom(topDirPath string) string {
if unversionedHome {
return topDirPath
}
return VersionedHome(topDirPath)
}
// IsVersionHome returns true if the Home path is versioned based on build.
func IsVersionHome() bool {
return !unversionedHome
}
// SetVersionHome sets if the Home path is versioned based on build.
//
// Used by the container subcommand to adjust the home path allowing state can be maintained between container
// restarts.
func SetVersionHome(version bool) {
unversionedHome = !version
}
// Config returns a directory where configuration file lives
func Config() string {
return configPath
}
// SetConfig overrides the Config path.
//
// Used by the container subcommand to adjust the overall config path allowing state can be maintained between container
// restarts.
func SetConfig(path string) {
configPath = path
}
// ConfigFile returns the path to the configuration file.
func ConfigFile() string {
return configFileWithDefaultOverride(DefaultConfigName)
}
// OtelConfigFile returns the path to the otel configuration file.
func OtelConfigFile() string {
return configFileWithDefaultOverride(DefaultOtelConfigName)
}
// configFileWithDefaultOverride returns the path to the configuration file overriding default value.
func configFileWithDefaultOverride(defaultConfig string) string {
if configFilePath == "" || configFilePath == DefaultConfigName {
return filepath.Join(Config(), defaultConfig)
}
if filepath.IsAbs(configFilePath) {
return configFilePath
}
return filepath.Join(Config(), configFilePath)
}
// ExternalInputs returns the path to load external inputs from.
func ExternalInputs() string {
return filepath.Join(Config(), ExternalInputsPattern)
}
// Data returns the data directory for Agent
func Data() string {
return DataFrom(Top())
}
// DataFrom returns the data directory for Agent using the passed directory as top path
func DataFrom(topDirPath string) string {
if unversionedHome {
// unversioned means the topPath is the data path
return topDirPath
}
return filepath.Join(topDirPath, "data")
}
// Run returns the run directory for Agent
func Run() string {
return filepath.Join(Home(), "run")
}
// Components returns the component directory for Agent
func Components() string {
return componentsPath
}
// Logs returns the log directory for Agent
func Logs() string {
return logsPath
}
// SetLogs updates the path for the logs.
func SetLogs(path string) {
logsPath = path
}
// VersionedHome returns a versioned path based on a TopPath and used commit.
func VersionedHome(base string) string {
versionedHomePath := filepath.Join(base, "data", fmt.Sprintf("elastic-agent-%s-%s", release.VersionWithSnapshot(), release.ShortCommit()))
_, err := os.Stat(versionedHomePath)
if errors.Is(err, os.ErrNotExist) {
// fallback to the legacy elastic-agent-<commit> path
versionedHomePath = filepath.Join(base, "data", fmt.Sprintf("elastic-agent-%s", release.ShortCommit()))
}
return versionedHomePath
}
// Downloads returns the downloads directory for Agent
func Downloads() string {
return downloadsPath
}
// SetDownloads updates the path for the downloads.
func SetDownloads(path string) {
downloadsPath = path
}
// Install returns the install directory for Agent
func Install() string {
if installPath == "" {
return filepath.Join(Home(), "install")
}
return installPath
}
// SetInstall updates the path for the install.
func SetInstall(path string) {
installPath = path
}
// ControlSocket returns the control socket directory for Agent
func ControlSocket() string {
return controlSocketPath
}
// SetControlSocket overrides the ControlSocket path.
//
// Used by the container subcommand to adjust the control socket path.
func SetControlSocket(path string) {
controlSocketPath = path
}
// initialTop returns the initial top-level path for the binary
//
// When nested in top-level/data/elastic-agent-${hash}/ the result is top-level/.
// The agent executable for MacOS is wrapped in the app bundle, so the path to the binary is
// top-level/data/elastic-agent-${hash}/elastic-agent.app/Contents/MacOS
func initialTop() string {
return ExecDir(retrieveExecutableDir())
}
// retrieveExecutablePath returns the executing binary, even if the started binary was a symlink
func retrieveExecutableDir() string {
execPath, err := os.Executable()
if err != nil {
panic(err)
}
evalPath, err := filepath.EvalSymlinks(execPath)
if err != nil {
panic(err)
}
return filepath.Dir(evalPath)
}
// isInsideData returns true when the exePath is inside of the current Agents data path.
func isInsideData(exeDir string) bool {
expectedDirLegacy := binaryDir(filepath.Join("data", fmt.Sprintf("elastic-agent-%s", release.ShortCommit())))
expectedDirWithVersion := binaryDir(filepath.Join("data", fmt.Sprintf("elastic-agent-%s-%s", release.VersionWithSnapshot(), release.ShortCommit())))
return strings.HasSuffix(exeDir, expectedDirLegacy) || strings.HasSuffix(exeDir, expectedDirWithVersion)
}
// ExecDir returns the "executable" directory which is:
// 1. The same if the execDir is not inside of the data path
// 2. Two levels up if the execDir inside of the data path on non-macOS platforms
// 3. Five levels up if the execDir inside of the dataPath on macOS platform
func ExecDir(execDir string) string {
if isInsideData(execDir) {
execDir = filepath.Dir(filepath.Dir(execDir))
if runtime.GOOS == darwin {
execDir = filepath.Dir(filepath.Dir(filepath.Dir(execDir)))
}
}
return execDir
}
// binaryDir returns the application binary directory
// For macOS it appends the path inside of the app bundle
// For other platforms it returns the same dir
func binaryDir(baseDir string) string {
if runtime.GOOS == darwin {
baseDir = filepath.Join(baseDir, "elastic-agent.app", "Contents", "MacOS")
}
return baseDir
}
// BinaryPath returns the application binary path that is concatenation of the directory and the agentName
func BinaryPath(baseDir, agentName string) string {
return filepath.Join(binaryDir(baseDir), agentName)
}
// TopBinaryPath returns the path to the Elastic Agent binary that is inside the Top directory.
//
// This always points to the symlink that points to the latest Elastic Agent version.
func TopBinaryPath() string {
return filepath.Join(Top(), BinaryName)
}
// RunningInstalled returns true when executing Agent is the installed Agent.
func RunningInstalled() bool {
// Check if install marker created by `elastic-agent install` exists
markerFilePath := filepath.Join(Top(), MarkerFileName)
if _, err := os.Stat(markerFilePath); err != nil {
return false
}
return true
}
// ControlSocketFromPath returns the control socket path for an Elastic Agent running
// on the defined platform, and its executing directory.
func ControlSocketFromPath(platform string, path string) string {
// socket should be inside this directory
socketPath := filepath.Join(path, ControlSocketName)
if platform == "windows" {
// on windows the control socket always uses the fallback
return utils.SocketURLWithFallback(socketPath, path)
}
unixSocket := fmt.Sprintf("unix://%s", socketPath)
if len(unixSocket) < 104 {
// small enough to fit
return unixSocket
}
// place in global /tmp to ensure that its small enough to fit; current path is way to long
// for it to be used, but needs to be unique per Agent (in the case that multiple are running)
return utils.SocketURLWithFallback(socketPath, path)
}
func pathSplit(path string) []string {
dir, file := filepath.Split(path)
if dir == "" && file == "" {
return []string{}
}
if dir == "" && file != "" {
return []string{file}
}
if dir == path {
return []string{}
}
return append(pathSplit(filepath.Clean(dir)), file)
}