diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 5d18889909e..cd2c99a11be 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -23,6 +23,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d - By default, all Beats-created files and folders will have a umask of 0027 (on POSIX systems). {pull}14119[14119] - Adding new `Enterprise` license type to the licenser. {issue}14246[14246] - Change wording when we fail to load a CA file to the cert pool. {issue}14309[14309] +- Allow Metricbeat's beat module to read monitoring information over a named pipe or unix domain socket. {pull}14558[14558] *Auditbeat* diff --git a/libbeat/api/npipe/listener_windows.go b/libbeat/api/npipe/listener_windows.go index c3c9db7a719..b72b4faad92 100644 --- a/libbeat/api/npipe/listener_windows.go +++ b/libbeat/api/npipe/listener_windows.go @@ -65,6 +65,13 @@ func DialContext(npipe string) func(context.Context, string, string) (net.Conn, } } +// Dial create a Dial to be use with an http.Client to connect to a pipe. +func Dial(npipe string) func(string, string) (net.Conn, error) { + return func(_, _ string) (net.Conn, error) { + return winio.DialPipe(npipe, nil) + } +} + // DefaultSD returns a default SecurityDescriptor which is the minimal required permissions to be // able to write to the named pipe. The security descriptor is returned in SDDL format. // diff --git a/libbeat/api/npipe/listerner_posix.go b/libbeat/api/npipe/listerner_posix.go index c8239fd914a..ee93e1c23a0 100644 --- a/libbeat/api/npipe/listerner_posix.go +++ b/libbeat/api/npipe/listerner_posix.go @@ -31,3 +31,10 @@ func DialContext(npipe string) func(context.Context, string, string) (net.Conn, return nil, errors.New("named pipe doesn't work on linux") } } + +// Dial create a Dial to be use with an http.Client to connect to a pipe. +func Dial(npipe string) func(string, string) (net.Conn, error) { + return func(_, _ string) (net.Conn, error) { + return nil, errors.New("named pipe doesn't work on linux") + } +} diff --git a/libbeat/docs/http-endpoint.asciidoc b/libbeat/docs/http-endpoint.asciidoc index e60a0abbe72..1264ce2d56e 100644 --- a/libbeat/docs/http-endpoint.asciidoc +++ b/libbeat/docs/http-endpoint.asciidoc @@ -35,7 +35,7 @@ You can query a unix socket using the `CURL` command and the `--unix-socket` fla [source,js] ---- -curl -XGET --unix-socket '/var/run/{beatname_lc}.sock' 'http://unix/stats/?pretty' +curl -XGET --unix-socket '/var/run/{beatname_lc}.sock' 'http:/stats/?pretty' ---- diff --git a/libbeat/outputs/transport/tcp.go b/libbeat/outputs/transport/tcp.go index b87bfb8b0de..b1e6024f8f1 100644 --- a/libbeat/outputs/transport/tcp.go +++ b/libbeat/outputs/transport/tcp.go @@ -45,7 +45,6 @@ func TestNetDialer(d testing.Driver, timeout time.Duration) Dialer { if err != nil { return nil, err } - addresses, err := net.LookupHost(host) d.Fatal("dns lookup", err) d.Info("addresses", strings.Join(addresses, ", ")) @@ -59,3 +58,16 @@ func TestNetDialer(d testing.Driver, timeout time.Duration) Dialer { return DialWith(dialer, network, host, addresses, port) }) } + +// UnixDialer creates a Unix Dialer when using unix domain socket. +func UnixDialer(timeout time.Duration, sockFile string) Dialer { + return TestUnixDialer(testing.NullDriver, timeout, sockFile) +} + +// TestUnixDialer creates a Test Unix Dialer when using domain socket. +func TestUnixDialer(d testing.Driver, timeout time.Duration, sockFile string) Dialer { + return DialerFunc(func(network, address string) (net.Conn, error) { + d.Info("connecting using unix domain socket", sockFile) + return net.DialTimeout("unix", sockFile, timeout) + }) +} diff --git a/metricbeat/helper/dialer/dialer.go b/metricbeat/helper/dialer/dialer.go new file mode 100644 index 00000000000..d2aaf0b6ed9 --- /dev/null +++ b/metricbeat/helper/dialer/dialer.go @@ -0,0 +1,59 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package dialer + +import ( + "fmt" + "time" + + "github.com/elastic/beats/libbeat/outputs/transport" +) + +// Builder is a dialer builder. +type Builder interface { + fmt.Stringer + Make(time.Duration) (transport.Dialer, error) +} + +// DefaultDialerBuilder create a builder to dialer over TCP and UDP. +type DefaultDialerBuilder struct{} + +// Make creates a dialer. +func (t *DefaultDialerBuilder) Make(timeout time.Duration) (transport.Dialer, error) { + return transport.NetDialer(timeout), nil +} + +func (t *DefaultDialerBuilder) String() string { + return "TCP/UDP" +} + +// NewDefaultDialerBuilder creates a DefaultDialerBuilder. +func NewDefaultDialerBuilder() *DefaultDialerBuilder { + return &DefaultDialerBuilder{} +} + +// NewNpipeDialerBuilder creates a NpipeDialerBuilder. +func NewNpipeDialerBuilder(path string) *NpipeDialerBuilder { + return &NpipeDialerBuilder{Path: path} +} + +// NewUnixDialerBuilder returns a new TransportUnix instance that will allow the HTTP client to communicate +// over a unix domain socket it require a valid path to the socket on the filesystem. +func NewUnixDialerBuilder(path string) *UnixDialerBuilder { + return &UnixDialerBuilder{Path: path} +} diff --git a/metricbeat/helper/dialer/dialer_posix.go b/metricbeat/helper/dialer/dialer_posix.go new file mode 100644 index 00000000000..4b6f61d1bfb --- /dev/null +++ b/metricbeat/helper/dialer/dialer_posix.go @@ -0,0 +1,57 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//+build !windows + +package dialer + +import ( + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/elastic/beats/libbeat/outputs/transport" +) + +// UnixDialerBuilder creates a builder to dial over unix domain socket. +type UnixDialerBuilder struct { + Path string +} + +// Make creates a dialer. +func (t *UnixDialerBuilder) Make(timeout time.Duration) (transport.Dialer, error) { + return transport.UnixDialer(timeout, strings.TrimSuffix(t.Path, "/")), nil +} + +func (t *UnixDialerBuilder) String() string { + return "Unix: " + t.Path +} + +// NpipeDialerBuilder creates a builder to dial over a named pipe. +type NpipeDialerBuilder struct { + Path string +} + +// Make creates a dialer. +func (t *NpipeDialerBuilder) Make(_ time.Duration) (transport.Dialer, error) { + return nil, errors.New("cannot the URI, named pipes are only supported on Windows") +} + +func (t *NpipeDialerBuilder) String() string { + return "Npipe: " + t.Path +} diff --git a/metricbeat/helper/dialer/dialer_windows.go b/metricbeat/helper/dialer/dialer_windows.go new file mode 100644 index 00000000000..421f91a6017 --- /dev/null +++ b/metricbeat/helper/dialer/dialer_windows.go @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//+build windows + +package dialer + +import ( + "net" + "strings" + "time" + + "github.com/pkg/errors" + + winio "github.com/Microsoft/go-winio" + + "github.com/elastic/beats/libbeat/api/npipe" + "github.com/elastic/beats/libbeat/outputs/transport" +) + +// UnixDialerBuilder creates a builder to dial over a unix domain socket. +type UnixDialerBuilder struct { + Path string +} + +// Make creates a dialer. +func (t *UnixDialerBuilder) Make(_ time.Duration) (transport.Dialer, error) { + return nil, errors.New( + "cannot use the URI, unix sockets are not supported on Windows, use npipe instead", + ) +} + +func (t *UnixDialerBuilder) String() string { + return "Unix: " + t.Path +} + +// NpipeDialerBuilder creates a builder to dial over a named pipe. +type NpipeDialerBuilder struct { + Path string +} + +func (t *NpipeDialerBuilder) String() string { + return "Npipe: " + t.Path +} + +// Make creates a dialer. +func (t *NpipeDialerBuilder) Make(timeout time.Duration) (transport.Dialer, error) { + to := timeout + return transport.DialerFunc( + func(_, _ string) (net.Conn, error) { + return winio.DialPipe( + strings.TrimSuffix(npipe.TransformString(t.Path), "/"), + &to, + ) + }, + ), nil +} diff --git a/metricbeat/helper/http.go b/metricbeat/helper/http.go index 17659442d7b..a65678325c5 100644 --- a/metricbeat/helper/http.go +++ b/metricbeat/helper/http.go @@ -30,9 +30,12 @@ import ( "github.com/elastic/beats/libbeat/common/transport/tlscommon" "github.com/elastic/beats/libbeat/outputs/transport" + "github.com/elastic/beats/metricbeat/helper/dialer" "github.com/elastic/beats/metricbeat/mb" ) +// HTTP is a custom HTTP Client that handle the complexity of connection and retrieving information +// from HTTP endpoint. type HTTP struct { hostData mb.HostData client *http.Client // HTTP client that is reused across requests. @@ -72,9 +75,18 @@ func newHTTPFromConfig(config Config, name string, hostData mb.HostData) (*HTTP, return nil, err } - var dialer, tlsDialer transport.Dialer + // Ensure backward compatibility + builder := hostData.Transport + if builder == nil { + builder = dialer.NewDefaultDialerBuilder() + } + + dialer, err := builder.Make(config.ConnectTimeout) + if err != nil { + return nil, err + } - dialer = transport.NetDialer(config.ConnectTimeout) + var tlsDialer transport.Dialer tlsDialer, err = transport.TLSDialer(dialer, tlsConfig, config.ConnectTimeout) if err != nil { return nil, err diff --git a/metricbeat/helper/http_test.go b/metricbeat/helper/http_test.go index 27a8419b3d9..bb6781eb916 100644 --- a/metricbeat/helper/http_test.go +++ b/metricbeat/helper/http_test.go @@ -18,16 +18,20 @@ package helper import ( + "fmt" "io/ioutil" + "net" "net/http" "net/http/httptest" "os" + "runtime" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/elastic/beats/metricbeat/helper/dialer" "github.com/elastic/beats/metricbeat/mb" ) @@ -158,6 +162,88 @@ func TestAuthentication(t *testing.T) { assert.Equal(t, http.StatusOK, response.StatusCode, "response status code") } +func TestOverUnixSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("unix domain socket aren't supported under Windows") + return + } + + t.Run("at root", func(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "testsocket") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + sockFile := tmpDir + "/test.sock" + + l, err := net.Listen("unix", sockFile) + require.NoError(t, err) + + defer l.Close() + + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "ehlo!") + }) + + go http.Serve(l, mux) + + cfg := defaultConfig() + hostData := mb.HostData{ + Transport: dialer.NewUnixDialerBuilder(sockFile), + URI: "http://unix/", + SanitizedURI: "http://unix", + } + + h, err := newHTTPFromConfig(cfg, "test", hostData) + require.NoError(t, err) + + r, err := h.FetchResponse() + require.NoError(t, err) + defer r.Body.Close() + content, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, []byte("ehlo!"), content) + }) + + t.Run("at specific path", func(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "testsocket") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + sockFile := tmpDir + "/test.sock" + uri := "http://unix/ok" + + l, err := net.Listen("unix", sockFile) + require.NoError(t, err) + + defer l.Close() + + mux := http.NewServeMux() + mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "ehlo!") + }) + + go http.Serve(l, mux) + + cfg := defaultConfig() + hostData := mb.HostData{ + Transport: dialer.NewUnixDialerBuilder(sockFile), + URI: uri, + SanitizedURI: uri, + } + + h, err := newHTTPFromConfig(cfg, "test", hostData) + require.NoError(t, err) + + r, err := h.FetchResponse() + require.NoError(t, err) + defer r.Body.Close() + content, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, []byte("ehlo!"), content) + }) +} + func checkTimeout(t *testing.T, h *HTTP) { t.Helper() diff --git a/metricbeat/helper/http_windows_test.go b/metricbeat/helper/http_windows_test.go new file mode 100644 index 00000000000..3d01f9c9a3b --- /dev/null +++ b/metricbeat/helper/http_windows_test.go @@ -0,0 +1,109 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//+build windows + +package helper + +import ( + "fmt" + "io/ioutil" + "net/http" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/beats/libbeat/api/npipe" + "github.com/elastic/beats/metricbeat/helper/dialer" + "github.com/elastic/beats/metricbeat/mb" +) + +func TestOverNamedpipe(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skipf("npipe is only supported under Windows") + return + } + + t.Run("at root", func(t *testing.T) { + p := `\\.\pipe\hellofromnpipe` + sd, err := npipe.DefaultSD("") + require.NoError(t, err) + l, err := npipe.NewListener(p, sd) + require.NoError(t, err) + defer l.Close() + + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "ehlo!") + }) + + go http.Serve(l, mux) + + cfg := defaultConfig() + hostData := mb.HostData{ + Transport: dialer.NewNpipeDialerBuilder(p), + URI: "http://npipe/", + SanitizedURI: "http://npipe/", + } + + h, err := newHTTPFromConfig(cfg, "test", hostData) + require.NoError(t, err) + + r, err := h.FetchResponse() + require.NoError(t, err) + defer r.Body.Close() + content, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, []byte("ehlo!"), content) + }) + + t.Run("at specific path", func(t *testing.T) { + p := `\\.\pipe\apath` + sd, err := npipe.DefaultSD("") + require.NoError(t, err) + l, err := npipe.NewListener(p, sd) + require.NoError(t, err) + defer l.Close() + + mux := http.NewServeMux() + mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "ehlo!") + }) + + go http.Serve(l, mux) + + cfg := defaultConfig() + hostData := mb.HostData{ + Transport: dialer.NewNpipeDialerBuilder(p), + URI: "http://npipe/ok", + SanitizedURI: "http://npipe/ok", + } + + h, err := newHTTPFromConfig(cfg, "test", hostData) + require.NoError(t, err) + + r, err := h.FetchResponse() + require.NoError(t, err) + defer r.Body.Close() + content, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, []byte("ehlo!"), content) + }) + +} diff --git a/metricbeat/mb/mb.go b/metricbeat/mb/mb.go index d55b8971d4a..24475e0e01a 100644 --- a/metricbeat/mb/mb.go +++ b/metricbeat/mb/mb.go @@ -30,6 +30,7 @@ import ( "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/logp" "github.com/elastic/beats/libbeat/monitoring" + "github.com/elastic/beats/metricbeat/helper/dialer" ) const ( @@ -236,8 +237,12 @@ type PushMetricSetV2WithContext interface { // HostData contains values parsed from the 'host' configuration. Other // configuration data like protocols, usernames, and passwords may also be -// used to construct this HostData data. +// used to construct this HostData data. HostData also contains information when combined scheme are +// used, like doing HTTP request over a UNIX socket. +// type HostData struct { + Transport dialer.Builder // The transport builder to use when creating the connection. + URI string // The full URI that should be used in connections. SanitizedURI string // A sanitized version of the URI without credentials. diff --git a/metricbeat/mb/parse/url.go b/metricbeat/mb/parse/url.go index 095cd4a43e0..fc32bc70a6f 100644 --- a/metricbeat/mb/parse/url.go +++ b/metricbeat/mb/parse/url.go @@ -24,6 +24,7 @@ import ( p "path" "strings" + "github.com/elastic/beats/metricbeat/helper/dialer" "github.com/elastic/beats/metricbeat/mb" "github.com/pkg/errors" @@ -98,6 +99,7 @@ func (b URLHostParserBuilder) Build() mb.HostParser { return mb.HostData{}, errors.Errorf("'basepath' config for module %v is not a string", module.Name()) } } + // Normalize basepath basePath = strings.Trim(basePath, "/") @@ -113,6 +115,12 @@ func (b URLHostParserBuilder) Build() mb.HostParser { // the HostData.Host field is set to the URLs path instead of the URLs host, // the same happens for "npipe". func NewHostDataFromURL(u *url.URL) mb.HostData { + return NewHostDataFromURLWithTransport(dialer.NewDefaultDialerBuilder(), u) +} + +// NewHostDataFromURLWithTransport Allow to specify what kind of transport to in conjonction of the +// url, this is useful if you use a combined scheme like "http+unix://" or "http+npipe". +func NewHostDataFromURLWithTransport(transport dialer.Builder, u *url.URL) mb.HostData { var user, pass string if u.User != nil { user = u.User.Username() @@ -125,6 +133,7 @@ func NewHostDataFromURL(u *url.URL) mb.HostData { } return mb.HostData{ + Transport: transport, URI: u.String(), SanitizedURI: redactURLCredentials(u).String(), Host: host, @@ -137,12 +146,13 @@ func NewHostDataFromURL(u *url.URL) mb.HostData { // defaults that are added to the URL if not present in the rawHost value. // Values from the rawHost take precedence over the defaults. func ParseURL(rawHost, scheme, user, pass, path, query string) (mb.HostData, error) { - u, err := getURL(rawHost, scheme, user, pass, path, query) + u, transport, err := getURL(rawHost, scheme, user, pass, path, query) + if err != nil { return mb.HostData{}, err } - return NewHostDataFromURL(u), nil + return NewHostDataFromURLWithTransport(transport, u), nil } // SetURLUser set the user credentials in the given URL. If the username or @@ -177,22 +187,46 @@ func SetURLUser(u *url.URL, defaultUser, defaultPass string) { // getURL constructs a URL from the rawHost value and adds the provided user, // password, path, and query params if one was not set in the rawURL value. -func getURL(rawURL, scheme, username, password, path, query string) (*url.URL, error) { +func getURL( + rawURL, scheme, username, password, path, query string, +) (*url.URL, dialer.Builder, error) { + if parts := strings.SplitN(rawURL, "://", 2); len(parts) != 2 { // Add scheme. rawURL = fmt.Sprintf("%s://%s", scheme, rawURL) } + var t dialer.Builder + u, err := url.Parse(rawURL) if err != nil { - return nil, fmt.Errorf("error parsing URL: %v", err) + return nil, t, fmt.Errorf("error parsing URL: %v", err) + } + + // discover the transport to use to communicate with the host if we have a combined scheme. + // possible values are mb.TransportTCP, mb.transportUnix or mb.TransportNpipe. + switch u.Scheme { + case "http+unix": + t = dialer.NewUnixDialerBuilder(u.Path) + u.Path = "" + u.Scheme = "http" + u.Host = "unix" + case "http+npipe": + p := strings.Replace(u.Path, "/pipe", `\\.pipe`, 1) + p = strings.Replace(p, "/", "\\", -1) + t = dialer.NewNpipeDialerBuilder(p) + u.Path = "" + u.Scheme = "http" + u.Host = "npipe" + default: + t = dialer.NewDefaultDialerBuilder() } SetURLUser(u, username, password) if !strings.HasSuffix(u.Scheme, "unix") && !strings.HasSuffix(u.Scheme, "npipe") { if u.Host == "" { - return nil, fmt.Errorf("error parsing URL: empty host") + return nil, t, fmt.Errorf("error parsing URL: empty host") } // Validate the host. The port is optional. @@ -201,11 +235,11 @@ func getURL(rawURL, scheme, username, password, path, query string) (*url.URL, e if strings.Contains(err.Error(), "missing port") { host = u.Host } else { - return nil, fmt.Errorf("error parsing URL: %v", err) + return nil, t, fmt.Errorf("error parsing URL: %v", err) } } if host == "" { - return nil, fmt.Errorf("error parsing URL: empty host") + return nil, t, fmt.Errorf("error parsing URL: empty host") } } @@ -220,7 +254,7 @@ func getURL(rawURL, scheme, username, password, path, query string) (*url.URL, e //Adds the query params in the url u, err = SetQueryParams(u, query) - return u, err + return u, t, err } // SetQueryParams adds the query params to existing query parameters overwriting any diff --git a/metricbeat/mb/parse/url_test.go b/metricbeat/mb/parse/url_test.go index 14ef42e1457..382f7e9cfec 100644 --- a/metricbeat/mb/parse/url_test.go +++ b/metricbeat/mb/parse/url_test.go @@ -20,6 +20,7 @@ package parse import ( "testing" + "github.com/elastic/beats/metricbeat/helper/dialer" "github.com/elastic/beats/metricbeat/mb" mbtest "github.com/elastic/beats/metricbeat/mb/testing" @@ -60,6 +61,66 @@ func TestParseURL(t *testing.T) { } }) + t.Run("http+unix at root", func(t *testing.T) { + rawURL := "http+unix:///var/lib/docker.sock" + hostData, err := ParseURL(rawURL, "http", "", "", "", "") + if assert.NoError(t, err) { + transport, ok := hostData.Transport.(*dialer.UnixDialerBuilder) + assert.True(t, ok) + assert.Equal(t, "/var/lib/docker.sock", transport.Path) + assert.Equal(t, "http://unix", hostData.URI) + assert.Equal(t, "http://unix", hostData.SanitizedURI) + assert.Equal(t, "unix", hostData.Host) + assert.Equal(t, "", hostData.User) + assert.Equal(t, "", hostData.Password) + } + }) + + t.Run("http+unix with path", func(t *testing.T) { + rawURL := "http+unix:///var/lib/docker.sock" + hostData, err := ParseURL(rawURL, "http", "", "", "apath", "") + if assert.NoError(t, err) { + transport, ok := hostData.Transport.(*dialer.UnixDialerBuilder) + assert.True(t, ok) + assert.Equal(t, "/var/lib/docker.sock", transport.Path) + assert.Equal(t, "http://unix/apath", hostData.URI) + assert.Equal(t, "http://unix/apath", hostData.SanitizedURI) + assert.Equal(t, "unix", hostData.Host) + assert.Equal(t, "", hostData.User) + assert.Equal(t, "", hostData.Password) + } + }) + + t.Run("http+npipe at root", func(t *testing.T) { + rawURL := "http+npipe://./pipe/custom" + hostData, err := ParseURL(rawURL, "http", "", "", "", "") + if assert.NoError(t, err) { + transport, ok := hostData.Transport.(*dialer.NpipeDialerBuilder) + assert.True(t, ok) + assert.Equal(t, `\\.pipe\custom`, transport.Path) + assert.Equal(t, "http://npipe", hostData.URI) + assert.Equal(t, "http://npipe", hostData.SanitizedURI) + assert.Equal(t, "npipe", hostData.Host) + assert.Equal(t, "", hostData.User) + assert.Equal(t, "", hostData.Password) + } + }) + + t.Run("http+npipe with path", func(t *testing.T) { + rawURL := "http+npipe://./pipe/custom" + hostData, err := ParseURL(rawURL, "http", "", "", "apath", "") + if assert.NoError(t, err) { + transport, ok := hostData.Transport.(*dialer.NpipeDialerBuilder) + assert.True(t, ok) + assert.Equal(t, `\\.pipe\custom`, transport.Path) + assert.Equal(t, "http://npipe/apath", hostData.URI) + assert.Equal(t, "http://npipe/apath", hostData.SanitizedURI) + assert.Equal(t, "npipe", hostData.Host) + assert.Equal(t, "", hostData.User) + assert.Equal(t, "", hostData.Password) + } + }) + t.Run("npipe", func(t *testing.T) { rawURL := "npipe://./pipe/docker_engine" hostData, err := ParseURL(rawURL, "tcp", "", "", "", "")