Skip to content

Commit

Permalink
revamp path handling
Browse files Browse the repository at this point in the history
chdir into workdir to avoid long paths for unix domain sockets
  • Loading branch information
malt3 committed Dec 2, 2024
1 parent 208fc8c commit ad5adf7
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 126 deletions.
204 changes: 134 additions & 70 deletions agent/locate/locate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,111 +6,175 @@ import (
"os"
"path"
"runtime"
"strings"

"github.com/tweag/credential-helper/api"
)

func Base() (string, error) {
if base, ok := os.LookupEnv(api.CredentialHelperInstallBase); ok {
return base, nil
// SetupEnvironment has to be called early in
// the agent and client process.
// It changes the working directory and exports
// environment variables to ensure
// a consistent working environment.
func SetupEnvironment() error {
workspacePath, err := setupWorkspaceDirectory()
if err != nil {
return err
}

// In Bazel integration tests we can't (and shouldn't)
// touch the user's home directory.
// Instead, we operate under $TEST_TMPDIR
var cacheDir string
var err error
cacheDir, ok := os.LookupEnv("TEST_TMPDIR")
if !ok {
// On a normal run, we want to operate in $HOME/.cache (or $XDG_CACHE_HOME)
cacheDir, err = os.UserCacheDir()
if err != nil {
return "", err
}
workdirPath, err := setupWorkdir(workspacePath)
if err != nil {
return err
}

workspaceDirectory, err := workspaceDirectory()
if err != nil {
return "", err
if err := os.MkdirAll(workdirPath, os.ModePerm); err != nil {
return err
}

return path.Join(cacheDir, "tweag-credential-helper", installBasePathComponent(workspaceDirectory)), nil
return os.Chdir(workdirPath)
}

func Bin() (string, error) {
base, err := Base()
if err != nil {
return "", err
func setupWorkspaceDirectory() (string, error) {
// try helper-specific workspace directory env var
workspacePath, haveWorkspacePath := os.LookupEnv(api.WorkspaceEnv)
if haveWorkspacePath {
return workspacePath, nil
}
return path.Join(base, "bin"), nil
}

func Run() (string, error) {
base, err := Base()
if err != nil {
return "", err
// maybe we are running under Bazel?
// in that case $BUILD_WORKSPACE_DIRECTORY is the root of the workspace
workspacePath, haveWorkspacePath = os.LookupEnv("BUILD_WORKSPACE_DIRECTORY")

if !haveWorkspacePath {
// as a last resort
// assume that current working directory
// is the Bazel workspace
var lookupErr error
workspacePath, lookupErr = os.Getwd()
if lookupErr != nil {
return "", lookupErr
}
}
return path.Join(base, "run"), nil
return workspacePath, os.Setenv(api.WorkspaceEnv, workspacePath)
}

func CredentialHelper() (string, error) {
if path, ok := os.LookupEnv(api.CredentialHelperBin); ok {
return path, nil
func setupWorkdir(workspacePath string) (string, error) {
// try helper-specifc workdir directory env var
workdirPath, haveWorkdirPath := os.LookupEnv(api.WorkdirEnv)
if haveWorkdirPath {
return workdirPath, nil
}
bin, err := Bin()
if err != nil {
return "", err
// assume that helper workdir
// is ${cache_dir}/tweag-credential-helper/${workdir_hash}
cacheDir := cacheDir()
workdirPath = path.Join(cacheDir, "tweag-credential-helper", workdirHash(workspacePath))
return workdirPath, os.Setenv(api.WorkdirEnv, workdirPath)

}

func LookupPathEnv(key, fallback string, shortPath bool) string {
unexpanded, ok := os.LookupEnv(key)
if !ok {
unexpanded = fallback
}
return expandPath(unexpanded, shortPath)
}

func Workdir() string {
return os.Getenv(api.WorkdirEnv)
}

func Bin() string {
return path.Join(Workdir(), "bin")
}

func Run() string {
return path.Join(Workdir(), "run")
}

func CredentialHelper() string {
filename := "credential-helper"
if runtime.GOOS == "windows" {
filename += ".exe"
}
return path.Join(bin, filename), nil

return LookupPathEnv(api.CredentialHelperBin, path.Join("%workdir%", "bin", filename), false)
}

func AgentPaths() (string, string, error) {
socketPath, haveSocketPathFromEnv := os.LookupEnv(api.AgentSocketPath)
pidPath, havePidPathFromEnv := os.LookupEnv(api.AgentPidPath)
run, err := Run()
if err != nil {
return "", "", err
func AgentPaths() (string, string) {
socketPath := LookupPathEnv(api.AgentSocketPath, path.Join("%workdir%", "run", "agent.sock"), true)
pidPath := LookupPathEnv(api.AgentPidPath, path.Join("%workdir%", "run", "agent.pid"), false)

return socketPath, pidPath
}

func tmpDir() string {
// In Bazel integration tests we can't (and shouldn't)
// touch /tmp
// Instead, we operate under $TEST_TMPDIR
if cacheDir, ok := os.LookupEnv("TEST_TMPDIR"); ok {
return cacheDir
}
if !haveSocketPathFromEnv {
socketPath = path.Join(run, "agent.sock")
if len(socketPath) >= 108 {
// In many environments
// we are not allowed to use
// a socket path longer than 108 bytes
//
// in those cases, fall back to a unique
// abstract uds (prefixed with @ in Go)
workspaceDirectory, err := workspaceDirectory()
if err != nil {
return "", "", err
}
socketPath = "@" + installBasePathComponent(workspaceDirectory)
}
return os.TempDir()
}

func cacheDir() string {
// In Bazel integration tests we can't (and shouldn't)
// touch the user's home directory.
// Instead, we operate under $TEST_TMPDIR
if cacheDir, ok := os.LookupEnv("TEST_TMPDIR"); ok {
return cacheDir
}
if !havePidPathFromEnv {
pidPath = path.Join(run, "agent.pid")
// On a normal run, we want to operate in $HOME/.cache (or $XDG_CACHE_HOME)
cacheDir, err := os.UserCacheDir()
if err != nil {
return tmpDir()
}
return cacheDir
}

return socketPath, pidPath, nil
func homeDir() string {
if home, err := os.UserHomeDir(); err == nil {
return home
}
// if we are in an environment where the current
// user doesn't have a resolvable
// home dir, it is probably safer to write to a temp
// dir instead.
return tmpDir()
}

func installBasePathComponent(workspaceDirectory string) string {
func workdirHash(workspaceDirectory string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(workspaceDirectory)))
}

func workspaceDirectory() (string, error) {
workspaceDirectory, ok := os.LookupEnv("BUILD_WORKSPACE_DIRECTORY")
if ok {
return workspaceDirectory, nil
func expandPath(input string, shortPath bool) string {
var prefix, suffix string
var canShortPath bool
switch {
case strings.HasPrefix(input, api.PlaceholderWorkdir):
prefix = os.Getenv(api.WorkdirEnv)
suffix = strings.TrimPrefix(input, api.PlaceholderWorkdir)
// we know that every process chdir's into
// the workdir early, so we can omit the prefix safely.
canShortPath = true
case strings.HasPrefix(input, api.PlaceholderWorkspaceDir):
prefix = os.Getenv(api.WorkspaceEnv)
suffix = strings.TrimPrefix(input, api.PlaceholderWorkspaceDir)
case strings.HasPrefix(input, api.PlaceholderTmpdir):
prefix = tmpDir()
suffix = strings.TrimPrefix(input, api.PlaceholderTmpdir)
case strings.HasPrefix(input, api.PlaceholderCachedir):
prefix = cacheDir()
suffix = strings.TrimPrefix(input, api.PlaceholderCachedir)
case strings.HasPrefix(input, api.PlaceholderHomedir):
prefix = homeDir()
suffix = strings.TrimPrefix(input, api.PlaceholderHomedir)
default:
return input
}
workspaceDirectory, err := os.Getwd()
if err != nil {
return "", err
if shortPath && canShortPath {
return path.Join(".", suffix)
}
return workspaceDirectory, nil
return path.Join(prefix, suffix)
}
25 changes: 19 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,25 @@ var CacheMiss = errors.New("cache miss")

// Environment variable names used by the credential helper.
const (
Standalone = "CREDENTIAL_HELPER_STANDALONE"
CredentialHelperInstallBase = "CREDENTIAL_HELPER_INSTALL_BASE"
CredentialHelperBin = "CREDENTIAL_HELPER_BIN"
AgentSocketPath = "CREDENTIAL_HELPER_AGENT_SOCKET"
AgentPidPath = "CREDENTIAL_HELPER_AGENT_PID"
LogLevelEnv = "CREDENTIAL_HELPER_LOGGING"
Standalone = "CREDENTIAL_HELPER_STANDALONE"
CredentialHelperBin = "CREDENTIAL_HELPER_BIN"
AgentSocketPath = "CREDENTIAL_HELPER_AGENT_SOCKET"
AgentPidPath = "CREDENTIAL_HELPER_AGENT_PID"
LogLevelEnv = "CREDENTIAL_HELPER_LOGGING"
// The working directory for the agent and client process.
// On startup, we chdir into it.
WorkdirEnv = "CREDENTIAL_HELPER_WORKDIR"
// The working directory of Bazel (path containing root module).
WorkspaceEnv = "CREDENTIAL_HELPER_WORKSPACE_DIRECTORY"
)

// Placeholders in configuration that is expanded automatically.
const (
PlaceholderWorkdir = "%workdir%"
PlaceholderWorkspaceDir = "%workspace%"
PlaceholderTmpdir = "%tmp%"
PlaceholderCachedir = "%cache%"
PlaceholderHomedir = "~"
)

// HelperFactory chooses a credential helper (like s3, gcs, github, ...) based on the raw uri.
Expand Down
18 changes: 6 additions & 12 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func Run(ctx context.Context, helperFactory api.HelperFactory, newCache api.NewC
os.Exit(1)
}
setLogLevel()
if err := locate.SetupEnvironment(); err != nil {
logging.Fatalf("setting up process environment: %v", err)
}
command := os.Args[1]
switch command {
case "get":
Expand Down Expand Up @@ -121,10 +124,7 @@ func launchOrConnectAgent() (api.Cache, func() error, error) {

logging.Debugf("launched agent")

sockPath, _, err := locate.AgentPaths()
if err != nil {
return nil, func() error { return nil }, err
}
sockPath, _ := locate.AgentPaths()
logging.Debugf("connecting to agent at %s", sockPath)
socketCache, err := cache.NewSocketCache(sockPath, time.Second)
if err != nil {
Expand All @@ -148,10 +148,7 @@ func clientProcess(ctx context.Context, helperFactory api.HelperFactory) {
}

func clientCommandProcess(command string, r io.Reader) {
socketPath, _, err := locate.AgentPaths()
if err != nil {
logging.Fatalf("%v", err)
}
socketPath, _ := locate.AgentPaths()
conn, err := agent.NewAgentCommandClient(socketPath)
if err != nil {
if command == api.AgentRequestShutdown {
Expand Down Expand Up @@ -186,10 +183,7 @@ func agentProcess(ctx context.Context, newCache api.NewCache) {
logging.Fatalf("running as agent is not supported in standalone mode")
}

sockPath, pidPath, err := locate.AgentPaths()
if err != nil {
logging.Fatalf("%v", err)
}
sockPath, pidPath := locate.AgentPaths()
service, cleanup, err := agent.NewCachingAgent(sockPath, pidPath, newCache())
if err != nil {
logging.Fatalf("%v", err)
Expand Down
27 changes: 3 additions & 24 deletions examples/customized/helper/cache/sqlitecache.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ type SqliteCache struct {
}

func NewSqliteCache() api.Cache {
dbFilePath, err := dbPath()
if err != nil {
panic(fmt.Sprintf("failed to find database path: %v", err))
}
dbFilePath := dbPath()
if dbFilePath != ":memory:" {
os.MkdirAll(path.Dir(dbFilePath), os.ModePerm)
}
Expand Down Expand Up @@ -119,24 +116,6 @@ func (c *SqliteCache) Prune(_ context.Context) error {
return nil
}

func dbPath() (string, error) {
dbPath, ok := os.LookupEnv("CREDENTIAL_HELPER_DB_PATH")
run, err := varDir()
if err != nil {
return "", err
}
if !ok {
dbPath = path.Join(run, "database.sqlite")
}

return dbPath, err
}

func varDir() (string, error) {
base, err := locate.Base()
if err != nil {
return "", err
}

return path.Join(base, "var"), nil
func dbPath() string {
return locate.LookupPathEnv("CREDENTIAL_HELPER_DB_PATH", path.Join("%workdir%", "var", "database.sqlite"), false)
}
9 changes: 4 additions & 5 deletions installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ func main() {
if err != nil {
fatalFmt("Failed to find %s: %v", pathFromEnv, err)
}

if _, err := os.Stat(path); err != nil {
fatalFmt("Failed to stat %s: %v", path, err)
}
if err := locate.SetupEnvironment(); err != nil {
fatalFmt("Failed to setup environment %s: %v", path, err)
}
destination, err := install(path)
if err != nil {
fatalFmt("Failed to install %s: %v", path, err)
Expand All @@ -29,10 +31,7 @@ func main() {
}

func install(credentialHelperBin string) (string, error) {
destination, err := locate.CredentialHelper()
if err != nil {
return "", err
}
destination := locate.CredentialHelper()
if err := os.MkdirAll(path.Dir(destination), 0o755); err != nil {
return "", err
}
Expand Down
Loading

0 comments on commit ad5adf7

Please sign in to comment.