feat(api): Add npipe support (#2018)

pull/2060/head
Olli Janatuinen 2018-07-20 12:02:06 +03:00 committed by Anthony Lapenna
parent 0368c4e937
commit 4129550d44
17 changed files with 133 additions and 43 deletions

View File

@ -16,8 +16,8 @@ import (
type Service struct{} type Service struct{}
const ( const (
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://") errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketNotFound = portainer.Error("Unable to locate Unix socket") errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe")
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk") errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval") errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
@ -116,15 +116,16 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
func validateEndpointURL(endpointURL string) error { func validateEndpointURL(endpointURL string) error {
if endpointURL != "" { 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 return errInvalidEndpointProtocol
} }
if strings.HasPrefix(endpointURL, "unix://") { if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://") socketPath := strings.TrimPrefix(endpointURL, "unix://")
socketPath = strings.TrimPrefix(socketPath, "npipe://")
if _, err := os.Stat(socketPath); err != nil { if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return errSocketNotFound return errSocketOrNamedPipeNotFound
} }
return err return err
} }

View File

@ -35,13 +35,13 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*clien
return createAgentClient(endpoint, factory.signatureService) return createAgentClient(endpoint, factory.signatureService)
} }
if strings.HasPrefix(endpoint.URL, "unix://") { if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return createUnixSocketClient(endpoint) return createLocalClient(endpoint)
} }
return createTCPClient(endpoint) return createTCPClient(endpoint)
} }
func createUnixSocketClient(endpoint *portainer.Endpoint) (*client.Client, error) { func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
return client.NewClientWithOpts( return client.NewClientWithOpts(
client.WithHost(endpoint.URL), client.WithHost(endpoint.URL),
client.WithVersion(portainer.SupportedDockerAPIVersion), client.WithVersion(portainer.SupportedDockerAPIVersion),

View File

@ -2,8 +2,8 @@ package endpoints
import ( import (
"net/http" "net/http"
"runtime"
"strconv" "strconv"
"strings"
"github.com/portainer/portainer" "github.com/portainer/portainer"
"github.com/portainer/portainer/crypto" "github.com/portainer/portainer/crypto"
@ -109,7 +109,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
} }
payload.AzureAuthenticationKey = azureAuthenticationKey payload.AzureAuthenticationKey = azureAuthenticationKey
default: default:
url, err := request.RetrieveMultiPartFormValue(r, "URL", false) url, err := request.RetrieveMultiPartFormValue(r, "URL", true)
if err != nil { if err != nil {
return portainer.Error("Invalid endpoint URL") 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) { func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
endpointType := portainer.DockerEnvironment 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) agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, nil)
if err != nil { if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err} return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err}

View File

@ -164,22 +164,32 @@ func createDial(endpoint *portainer.Endpoint) (net.Conn, error) {
return nil, err return nil, err
} }
var host string host := url.Host
if url.Scheme == "tcp" {
host = url.Host if url.Scheme == "unix" || url.Scheme == "npipe" {
} else if url.Scheme == "unix" {
host = url.Path host = url.Path
} }
var (
dial net.Conn
dialErr error
)
if endpoint.TLSConfig.TLS { if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil { if err != nil {
return nil, err 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) { func createExecStartRequest(execID string) (*http.Request, error) {

View File

@ -0,0 +1,11 @@
// +build linux
package websocket
import (
"net"
)
func createWinDial(host string) (net.Conn, error) {
return nil, nil
}

View File

@ -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)
}

View File

@ -58,21 +58,6 @@ func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool
return factory.createDockerReverseProxy(u, enableSignature) 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 { func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool) *httputil.ReverseProxy {
proxy := newSingleHostReverseProxyWithHostHeader(u) proxy := newSingleHostReverseProxyWithHostHeader(u)
transport := &proxyTransport{ transport := &proxyTransport{

View File

@ -1,6 +1,5 @@
package proxy package proxy
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
import ( import (
"io" "io"
"log" "log"
@ -9,11 +8,11 @@ import (
httperror "github.com/portainer/portainer/http/error" httperror "github.com/portainer/portainer/http/error"
) )
type socketProxy struct { type localProxy struct {
Transport *proxyTransport 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 // Force URL/domain to http/unixsocket to be able to
// use http.Transport RoundTrip to do the requests via the socket // use http.Transport RoundTrip to do the requests via the socket
r.URL.Scheme = "http" r.URL.Scheme = "http"

View File

@ -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
}

View File

@ -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)
},
}
}

View File

@ -51,8 +51,7 @@ func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *porta
} }
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false), nil return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false), nil
} }
// Assume unix:// scheme return manager.proxyFactory.newLocalProxy(endpointURL.Path), nil
return manager.proxyFactory.newDockerSocketProxy(endpointURL.Path), nil
} }
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) { func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {

View File

@ -247,7 +247,8 @@ paths:
- name: "URL" - name: "URL"
in: "formData" in: "formData"
type: "string" 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" - name: "PublicURL"
in: "formData" in: "formData"
type: "string" type: "string"

View File

@ -57,7 +57,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
service.createLocalEndpoint = function() { service.createLocalEndpoint = function() {
var deferred = $q.defer(); 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) { .then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}) })

View File

@ -67,7 +67,7 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
}) })
.then(function success(data) { .then(function success(data) {
var endpoint = data.endpoint; var endpoint = data.endpoint;
if (endpoint.URL.indexOf('unix://') === 0) { if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) {
$scope.endpointType = 'local'; $scope.endpointType = 'local';
} else { } else {
$scope.endpointType = 'remote'; $scope.endpointType = 'remote';

View File

@ -77,11 +77,20 @@
<div class="col-sm-12"> <div class="col-sm-12">
<span class="small"> <span class="small">
<p class="text-primary"> <p class="text-primary">
Manage the Docker environment where Portainer is running using the Unix filesystem socket. Manage the Docker environment where Portainer is running.
</p> </p>
<p class="text-muted"> <p class="text-muted">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Ensure that you have started the Portainer container with the following Docker flag: <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code>. Ensure that you have started the Portainer container with the following Docker flag:
</p>
<p class="text-muted">
<code>-v "/var/run/docker.sock:/var/run/docker.sock"</code> (Linux).
</p>
<p class="text-muted">
or
</p>
<p class="text-muted">
<code>-v \\.\pipe\docker_engine:\\.\pipe\docker_engine</code> (Windows).
</p> </p>
</span> </span>
</div> </div>

View File

@ -30,7 +30,7 @@ function ($scope, $state, EndpointService, StateManager, Notifications) {
$scope.createLocalEndpoint = function() { $scope.createLocalEndpoint = function() {
var name = 'local'; var name = 'local';
var URL = 'unix:///var/run/docker.sock'; var URL = '';
var endpoint; var endpoint;
$scope.state.actionInProgress = true; $scope.state.actionInProgress = true;

View File

@ -1,5 +1,7 @@
FROM microsoft/nanoserver FROM microsoft/nanoserver
USER ContainerAdministrator
COPY dist / COPY dist /
VOLUME C:\\data VOLUME C:\\data