Skip to content

Commit

Permalink
Merge pull request #5424 from jsternberg/wsl-socket-path
Browse files Browse the repository at this point in the history
command: check for wsl mount path on windows
  • Loading branch information
thaJeztah authored Sep 12, 2024
2 parents b0333da + 38c3fef commit a18c896
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 8 deletions.
120 changes: 112 additions & 8 deletions cli/command/telemetry_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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") != ""
}
29 changes: 29 additions & 0 deletions cli/command/telemetry_docker_test.go
Original file line number Diff line number Diff line change
@@ -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), "")
}

0 comments on commit a18c896

Please sign in to comment.