Skip to content

Commit 38c3fef

Browse files
committed
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 <jonathan.sternberg@docker.com>
1 parent 6372ec9 commit 38c3fef

2 files changed

Lines changed: 141 additions & 8 deletions

File tree

cli/command/telemetry_docker.go

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ package command
55

66
import (
77
"context"
8+
"fmt"
9+
"io/fs"
810
"net/url"
911
"os"
1012
"path"
13+
"path/filepath"
14+
"strings"
15+
"unicode"
1116

1217
"github.com/pkg/errors"
1318
"go.opentelemetry.io/otel"
@@ -77,14 +82,7 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) {
7782

7883
switch u.Scheme {
7984
case "unix":
80-
// Unix sockets are a bit weird. OTEL seems to imply they
81-
// can be used as an environment variable and are handled properly,
82-
// but they don't seem to be as the behavior of the environment variable
83-
// is to strip the scheme from the endpoint, but the underlying implementation
84-
// needs the scheme to use the correct resolver.
85-
//
86-
// We'll just handle this in a special way and add the unix:// back to the endpoint.
87-
endpoint = "unix://" + path.Join(u.Host, u.Path)
85+
endpoint = unixSocketEndpoint(u)
8886
case "https":
8987
secure = true
9088
fallthrough
@@ -135,3 +133,109 @@ func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
135133
}
136134
return []sdkmetric.Option{sdkmetric.WithReader(newCLIReader(exp))}
137135
}
136+
137+
// unixSocketEndpoint converts the unix scheme from URL to
138+
// an OTEL endpoint that can be used with the OTLP exporter.
139+
//
140+
// The OTLP exporter handles unix sockets in a strange way.
141+
// It seems to imply they can be used as an environment variable
142+
// and are handled properly, but they don't seem to be as the behavior
143+
// of the environment variable is to strip the scheme from the endpoint
144+
// while the underlying implementation needs the scheme to use the
145+
// correct resolver.
146+
func unixSocketEndpoint(u *url.URL) string {
147+
// GRPC does not allow host to be used.
148+
socketPath := u.Path
149+
150+
// If we are on windows and we have an absolute path
151+
// that references a letter drive, check to see if the
152+
// WSL equivalent path exists and we should use that instead.
153+
if isWsl() {
154+
if p := wslSocketPath(socketPath, os.DirFS("/")); p != "" {
155+
socketPath = p
156+
}
157+
}
158+
// Enforce that we are using forward slashes.
159+
return "unix://" + filepath.ToSlash(socketPath)
160+
}
161+
162+
// wslSocketPath will convert the referenced URL to a WSL-compatible
163+
// path and check if that path exists. If the path exists, it will
164+
// be returned.
165+
func wslSocketPath(s string, f fs.FS) string {
166+
if p := toWslPath(s); p != "" {
167+
if _, err := stat(p, f); err == nil {
168+
return "/" + p
169+
}
170+
}
171+
return ""
172+
}
173+
174+
// toWslPath converts the referenced URL to a WSL-compatible
175+
// path if this looks like a Windows absolute path.
176+
//
177+
// If no drive is in the URL, defaults to the C drive.
178+
func toWslPath(s string) string {
179+
drive, p, ok := parseUNCPath(s)
180+
if !ok {
181+
return ""
182+
}
183+
return fmt.Sprintf("mnt/%s%s", drive, p)
184+
}
185+
186+
func parseUNCPath(s string) (drive, p string, ok bool) {
187+
// UNC paths use backslashes but we're using forward slashes
188+
// so also enforce that here.
189+
//
190+
// In reality, this should have been enforced much earlier
191+
// than here since backslashes aren't allowed in URLs, but
192+
// we're going to code defensively here.
193+
s = filepath.ToSlash(s)
194+
195+
const uncPrefix = "//./"
196+
if !strings.HasPrefix(s, uncPrefix) {
197+
// Not a UNC path.
198+
return "", "", false
199+
}
200+
s = s[len(uncPrefix):]
201+
202+
parts := strings.SplitN(s, "/", 2)
203+
if len(parts) != 2 {
204+
// Not enough components.
205+
return "", "", false
206+
}
207+
208+
drive, ok = splitWindowsDrive(parts[0])
209+
if !ok {
210+
// Not a windows drive.
211+
return "", "", false
212+
}
213+
return drive, "/" + parts[1], true
214+
}
215+
216+
// splitWindowsDrive checks if the string references a windows
217+
// drive (such as c:) and returns the drive letter if it is.
218+
func splitWindowsDrive(s string) (string, bool) {
219+
if b := []rune(s); len(b) == 2 && unicode.IsLetter(b[0]) && b[1] == ':' {
220+
return string(b[0]), true
221+
}
222+
return "", false
223+
}
224+
225+
func stat(p string, f fs.FS) (fs.FileInfo, error) {
226+
if f, ok := f.(fs.StatFS); ok {
227+
return f.Stat(p)
228+
}
229+
230+
file, err := f.Open(p)
231+
if err != nil {
232+
return nil, err
233+
}
234+
235+
defer file.Close()
236+
return file.Stat()
237+
}
238+
239+
func isWsl() bool {
240+
return os.Getenv("WSL_DISTRO_NAME") != ""
241+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package command
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
"testing/fstest"
7+
8+
"gotest.tools/v3/assert"
9+
)
10+
11+
func TestWslSocketPath(t *testing.T) {
12+
u, err := url.Parse("unix:////./c:/my/file/path")
13+
assert.NilError(t, err)
14+
15+
// Ensure host is empty.
16+
assert.Equal(t, u.Host, "")
17+
18+
// Use a filesystem where the WSL path exists.
19+
fs := fstest.MapFS{
20+
"mnt/c/my/file/path": {},
21+
}
22+
assert.Equal(t, wslSocketPath(u.Path, fs), "/mnt/c/my/file/path")
23+
24+
// Use a filesystem where the WSL path doesn't exist.
25+
fs = fstest.MapFS{
26+
"my/file/path": {},
27+
}
28+
assert.Equal(t, wslSocketPath(u.Path, fs), "")
29+
}

0 commit comments

Comments
 (0)