feat(api): Add npipe support (#2018)
parent
0368c4e937
commit
4129550d44
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package websocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createWinDial(host string) (net.Conn, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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{
|
||||||
|
|
|
@ -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"
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
})
|
})
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
FROM microsoft/nanoserver
|
FROM microsoft/nanoserver
|
||||||
|
|
||||||
|
USER ContainerAdministrator
|
||||||
|
|
||||||
COPY dist /
|
COPY dist /
|
||||||
|
|
||||||
VOLUME C:\\data
|
VOLUME C:\\data
|
||||||
|
|
Loading…
Reference in New Issue