From 38c3fef1a89cc31dee66e879010f2cec4fa04a0d Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Thu, 12 Sep 2024 11:14:43 -0500 Subject: [PATCH] command: check for wsl mount path on windows This checks for the equivalent WSL mount path on windows. WSL will mount the windows drives at `/mnt/c` (or whichever drive is being used). This is done by parsing a UNC path with forward slashes from the unix socket URL. Signed-off-by: Jonathan A. Sternberg --- cli/command/telemetry_docker.go | 120 +++++++++++++++++++++++++-- cli/command/telemetry_docker_test.go | 29 +++++++ 2 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 cli/command/telemetry_docker_test.go diff --git a/cli/command/telemetry_docker.go b/cli/command/telemetry_docker.go index 94ab3a3929..8c50dc3ef6 100644 --- a/cli/command/telemetry_docker.go +++ b/cli/command/telemetry_docker.go @@ -5,9 +5,14 @@ package command import ( "context" + "fmt" + "io/fs" "net/url" "os" "path" + "path/filepath" + "strings" + "unicode" "github.com/pkg/errors" "go.opentelemetry.io/otel" @@ -77,14 +82,7 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) { switch u.Scheme { case "unix": - // Unix sockets are a bit weird. OTEL seems to imply they - // can be used as an environment variable and are handled properly, - // but they don't seem to be as the behavior of the environment variable - // is to strip the scheme from the endpoint, but the underlying implementation - // needs the scheme to use the correct resolver. - // - // We'll just handle this in a special way and add the unix:// back to the endpoint. - endpoint = "unix://" + path.Join(u.Host, u.Path) + endpoint = unixSocketEndpoint(u) case "https": secure = true fallthrough @@ -135,3 +133,109 @@ func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option { } return []sdkmetric.Option{sdkmetric.WithReader(newCLIReader(exp))} } + +// unixSocketEndpoint converts the unix scheme from URL to +// an OTEL endpoint that can be used with the OTLP exporter. +// +// The OTLP exporter handles unix sockets in a strange way. +// It seems to imply they can be used as an environment variable +// and are handled properly, but they don't seem to be as the behavior +// of the environment variable is to strip the scheme from the endpoint +// while the underlying implementation needs the scheme to use the +// correct resolver. +func unixSocketEndpoint(u *url.URL) string { + // GRPC does not allow host to be used. + socketPath := u.Path + + // If we are on windows and we have an absolute path + // that references a letter drive, check to see if the + // WSL equivalent path exists and we should use that instead. + if isWsl() { + if p := wslSocketPath(socketPath, os.DirFS("/")); p != "" { + socketPath = p + } + } + // Enforce that we are using forward slashes. + return "unix://" + filepath.ToSlash(socketPath) +} + +// wslSocketPath will convert the referenced URL to a WSL-compatible +// path and check if that path exists. If the path exists, it will +// be returned. +func wslSocketPath(s string, f fs.FS) string { + if p := toWslPath(s); p != "" { + if _, err := stat(p, f); err == nil { + return "/" + p + } + } + return "" +} + +// toWslPath converts the referenced URL to a WSL-compatible +// path if this looks like a Windows absolute path. +// +// If no drive is in the URL, defaults to the C drive. +func toWslPath(s string) string { + drive, p, ok := parseUNCPath(s) + if !ok { + return "" + } + return fmt.Sprintf("mnt/%s%s", drive, p) +} + +func parseUNCPath(s string) (drive, p string, ok bool) { + // UNC paths use backslashes but we're using forward slashes + // so also enforce that here. + // + // In reality, this should have been enforced much earlier + // than here since backslashes aren't allowed in URLs, but + // we're going to code defensively here. + s = filepath.ToSlash(s) + + const uncPrefix = "//./" + if !strings.HasPrefix(s, uncPrefix) { + // Not a UNC path. + return "", "", false + } + s = s[len(uncPrefix):] + + parts := strings.SplitN(s, "/", 2) + if len(parts) != 2 { + // Not enough components. + return "", "", false + } + + drive, ok = splitWindowsDrive(parts[0]) + if !ok { + // Not a windows drive. + return "", "", false + } + return drive, "/" + parts[1], true +} + +// splitWindowsDrive checks if the string references a windows +// drive (such as c:) and returns the drive letter if it is. +func splitWindowsDrive(s string) (string, bool) { + if b := []rune(s); len(b) == 2 && unicode.IsLetter(b[0]) && b[1] == ':' { + return string(b[0]), true + } + return "", false +} + +func stat(p string, f fs.FS) (fs.FileInfo, error) { + if f, ok := f.(fs.StatFS); ok { + return f.Stat(p) + } + + file, err := f.Open(p) + if err != nil { + return nil, err + } + + defer file.Close() + return file.Stat() +} + +func isWsl() bool { + return os.Getenv("WSL_DISTRO_NAME") != "" +} diff --git a/cli/command/telemetry_docker_test.go b/cli/command/telemetry_docker_test.go new file mode 100644 index 0000000000..c6501e4ed9 --- /dev/null +++ b/cli/command/telemetry_docker_test.go @@ -0,0 +1,29 @@ +package command + +import ( + "net/url" + "testing" + "testing/fstest" + + "gotest.tools/v3/assert" +) + +func TestWslSocketPath(t *testing.T) { + u, err := url.Parse("unix:////./c:/my/file/path") + assert.NilError(t, err) + + // Ensure host is empty. + assert.Equal(t, u.Host, "") + + // Use a filesystem where the WSL path exists. + fs := fstest.MapFS{ + "mnt/c/my/file/path": {}, + } + assert.Equal(t, wslSocketPath(u.Path, fs), "/mnt/c/my/file/path") + + // Use a filesystem where the WSL path doesn't exist. + fs = fstest.MapFS{ + "my/file/path": {}, + } + assert.Equal(t, wslSocketPath(u.Path, fs), "") +}