diff --git a/api/cli/cli.go b/api/cli/cli.go index c1792a29d..7c6febac2 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -16,8 +16,8 @@ import ( type Service struct{} const ( - errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://") - errSocketNotFound = portainer.Error("Unable to locate Unix socket") + errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") + errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe") errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk") errInvalidSyncInterval = portainer.Error("Invalid synchronization interval") @@ -116,15 +116,16 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { func validateEndpointURL(endpointURL string) error { if endpointURL != "" { - if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") { + if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") { return errInvalidEndpointProtocol } - if strings.HasPrefix(endpointURL, "unix://") { + if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") { socketPath := strings.TrimPrefix(endpointURL, "unix://") + socketPath = strings.TrimPrefix(socketPath, "npipe://") if _, err := os.Stat(socketPath); err != nil { if os.IsNotExist(err) { - return errSocketNotFound + return errSocketOrNamedPipeNotFound } return err } diff --git a/api/docker/client.go b/api/docker/client.go index 4538f981e..af9f08c46 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -35,13 +35,13 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*clien return createAgentClient(endpoint, factory.signatureService) } - if strings.HasPrefix(endpoint.URL, "unix://") { - return createUnixSocketClient(endpoint) + if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") { + return createLocalClient(endpoint) } return createTCPClient(endpoint) } -func createUnixSocketClient(endpoint *portainer.Endpoint) (*client.Client, error) { +func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) { return client.NewClientWithOpts( client.WithHost(endpoint.URL), client.WithVersion(portainer.SupportedDockerAPIVersion), diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 55f0ed879..6c1e1f894 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -2,8 +2,8 @@ package endpoints import ( "net/http" + "runtime" "strconv" - "strings" "github.com/portainer/portainer" "github.com/portainer/portainer/crypto" @@ -109,7 +109,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.AzureAuthenticationKey = azureAuthenticationKey default: - url, err := request.RetrieveMultiPartFormValue(r, "URL", false) + url, err := request.RetrieveMultiPartFormValue(r, "URL", true) if err != nil { return portainer.Error("Invalid endpoint URL") } @@ -192,7 +192,12 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointType := portainer.DockerEnvironment - if !strings.HasPrefix(payload.URL, "unix://") { + if payload.URL == "" { + payload.URL = "unix:///var/run/docker.sock" + if runtime.GOOS == "windows" { + payload.URL = "npipe:////./pipe/docker_engine" + } + } else { agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, nil) if err != nil { return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err} diff --git a/api/http/handler/websocket/websocket_exec.go b/api/http/handler/websocket/websocket_exec.go index 6e46084a6..331c62ac4 100644 --- a/api/http/handler/websocket/websocket_exec.go +++ b/api/http/handler/websocket/websocket_exec.go @@ -164,22 +164,32 @@ func createDial(endpoint *portainer.Endpoint) (net.Conn, error) { return nil, err } - var host string - if url.Scheme == "tcp" { - host = url.Host - } else if url.Scheme == "unix" { + host := url.Host + + if url.Scheme == "unix" || url.Scheme == "npipe" { host = url.Path } + var ( + dial net.Conn + dialErr error + ) + if endpoint.TLSConfig.TLS { tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) if err != nil { return nil, err } - return tls.Dial(url.Scheme, host, tlsConfig) + dial, dialErr = tls.Dial(url.Scheme, host, tlsConfig) + } else { + if url.Scheme == "npipe" { + dial, dialErr = createWinDial(host) + } else { + dial, dialErr = net.Dial(url.Scheme, host) + } } - return net.Dial(url.Scheme, host) + return dial, dialErr } func createExecStartRequest(execID string) (*http.Request, error) { diff --git a/api/http/handler/websocket/websocket_exec_linux.go b/api/http/handler/websocket/websocket_exec_linux.go new file mode 100644 index 000000000..99b3e9e7a --- /dev/null +++ b/api/http/handler/websocket/websocket_exec_linux.go @@ -0,0 +1,11 @@ +// +build linux + +package websocket + +import ( + "net" +) + +func createWinDial(host string) (net.Conn, error) { + return nil, nil +} diff --git a/api/http/handler/websocket/websocket_exec_windows.go b/api/http/handler/websocket/websocket_exec_windows.go new file mode 100644 index 000000000..3c9122d98 --- /dev/null +++ b/api/http/handler/websocket/websocket_exec_windows.go @@ -0,0 +1,13 @@ +// +build windows + +package websocket + +import ( + "net" + + "github.com/Microsoft/go-winio" +) + +func createWinDial(host string) (net.Conn, error) { + return winio.DialPipe(host, nil) +} diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index f3c43510f..73593f836 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -58,21 +58,6 @@ func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool return factory.createDockerReverseProxy(u, enableSignature) } -func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler { - proxy := &socketProxy{} - transport := &proxyTransport{ - enableSignature: false, - ResourceControlService: factory.ResourceControlService, - TeamMembershipService: factory.TeamMembershipService, - SettingsService: factory.SettingsService, - RegistryService: factory.RegistryService, - DockerHubService: factory.DockerHubService, - dockerTransport: newSocketTransport(path), - } - proxy.Transport = transport - return proxy -} - func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool) *httputil.ReverseProxy { proxy := newSingleHostReverseProxyWithHostHeader(u) transport := &proxyTransport{ diff --git a/api/http/proxy/socket.go b/api/http/proxy/local.go similarity index 81% rename from api/http/proxy/socket.go rename to api/http/proxy/local.go index 58ccdec27..8a7f5842d 100644 --- a/api/http/proxy/socket.go +++ b/api/http/proxy/local.go @@ -1,6 +1,5 @@ package proxy -// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket import ( "io" "log" @@ -9,11 +8,11 @@ import ( httperror "github.com/portainer/portainer/http/error" ) -type socketProxy struct { +type localProxy struct { Transport *proxyTransport } -func (proxy *socketProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (proxy *localProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Force URL/domain to http/unixsocket to be able to // use http.Transport RoundTrip to do the requests via the socket r.URL.Scheme = "http" diff --git a/api/http/proxy/local_linux.go b/api/http/proxy/local_linux.go new file mode 100644 index 000000000..af8ed07d6 --- /dev/null +++ b/api/http/proxy/local_linux.go @@ -0,0 +1,22 @@ +// +build linux + +package proxy + +import ( + "net/http" +) + +func (factory *proxyFactory) newLocalProxy(path string) http.Handler { + proxy := &localProxy{} + transport := &proxyTransport{ + enableSignature: false, + ResourceControlService: factory.ResourceControlService, + TeamMembershipService: factory.TeamMembershipService, + SettingsService: factory.SettingsService, + RegistryService: factory.RegistryService, + DockerHubService: factory.DockerHubService, + dockerTransport: newSocketTransport(path), + } + proxy.Transport = transport + return proxy +} diff --git a/api/http/proxy/local_windows.go b/api/http/proxy/local_windows.go new file mode 100644 index 000000000..5e98e47ac --- /dev/null +++ b/api/http/proxy/local_windows.go @@ -0,0 +1,33 @@ +// +build windows + +package proxy + +import ( + "net" + "net/http" + + "github.com/Microsoft/go-winio" +) + +func (factory *proxyFactory) newLocalProxy(path string) http.Handler { + proxy := &localProxy{} + transport := &proxyTransport{ + enableSignature: false, + ResourceControlService: factory.ResourceControlService, + TeamMembershipService: factory.TeamMembershipService, + SettingsService: factory.SettingsService, + RegistryService: factory.RegistryService, + DockerHubService: factory.DockerHubService, + dockerTransport: newNamedPipeTransport(path), + } + proxy.Transport = transport + return proxy +} + +func newNamedPipeTransport(namedPipePath string) *http.Transport { + return &http.Transport{ + Dial: func(proto, addr string) (conn net.Conn, err error) { + return winio.DialPipe(namedPipePath, nil) + }, + } +} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 88acdf515..eb60a5dc0 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -51,8 +51,7 @@ func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *porta } return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false), nil } - // Assume unix:// scheme - return manager.proxyFactory.newDockerSocketProxy(endpointURL.Path), nil + return manager.proxyFactory.newLocalProxy(endpointURL.Path), nil } func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) { diff --git a/api/swagger.yaml b/api/swagger.yaml index d0cbbe5bd..d936a710c 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -247,7 +247,8 @@ paths: - name: "URL" in: "formData" type: "string" - description: "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Required if endpoint type is set to 1 or 2." + description: "URL or IP address of a Docker host (example: docker.mydomain.tld:2375).\ + \ Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)" - name: "PublicURL" in: "formData" type: "string" diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index e7dd9bb97..003674240 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -57,7 +57,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { service.createLocalEndpoint = function() { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', 1, 'unix:///var/run/docker.sock', '', 1, [], false) + FileUploadService.createEndpoint('local', 1, '', '', 1, [], false) .then(function success(response) { deferred.resolve(response.data); }) diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 378924ccf..137bac012 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -67,7 +67,7 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi }) .then(function success(data) { var endpoint = data.endpoint; - if (endpoint.URL.indexOf('unix://') === 0) { + if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) { $scope.endpointType = 'local'; } else { $scope.endpointType = 'remote'; diff --git a/app/portainer/views/init/endpoint/initEndpoint.html b/app/portainer/views/init/endpoint/initEndpoint.html index 7de38d64e..f6fdb652e 100644 --- a/app/portainer/views/init/endpoint/initEndpoint.html +++ b/app/portainer/views/init/endpoint/initEndpoint.html @@ -77,11 +77,20 @@

- Manage the Docker environment where Portainer is running using the Unix filesystem socket. + Manage the Docker environment where Portainer is running.

- Ensure that you have started the Portainer container with the following Docker flag: -v "/var/run/docker.sock:/var/run/docker.sock". + Ensure that you have started the Portainer container with the following Docker flag: +

+

+ -v "/var/run/docker.sock:/var/run/docker.sock" (Linux). +

+

+ or +

+

+ -v \\.\pipe\docker_engine:\\.\pipe\docker_engine (Windows).

diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index 0c7bf1a03..9a7b64276 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -30,7 +30,7 @@ function ($scope, $state, EndpointService, StateManager, Notifications) { $scope.createLocalEndpoint = function() { var name = 'local'; - var URL = 'unix:///var/run/docker.sock'; + var URL = ''; var endpoint; $scope.state.actionInProgress = true; diff --git a/build/windows/nanoserver/Dockerfile b/build/windows/nanoserver/Dockerfile index ae8a25a39..36919634c 100644 --- a/build/windows/nanoserver/Dockerfile +++ b/build/windows/nanoserver/Dockerfile @@ -1,5 +1,7 @@ FROM microsoft/nanoserver +USER ContainerAdministrator + COPY dist / VOLUME C:\\data