feat(api): ping the endpoint at creation time (#1817)

pull/938/merge
Anthony Lapenna 2018-04-16 13:19:24 +02:00 committed by GitHub
parent 031b428e0c
commit 05d6abf57b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 270 additions and 109 deletions

View File

@ -8,6 +8,27 @@ import (
"github.com/portainer/portainer"
)
func CreateTLSConfig(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config.InsecureSkipVerify = skipServerVerification
if !skipClientVerification {
certificate, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
config.Certificates = []tls.Certificate{certificate}
}
if !skipServerVerification {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config.RootCAs = caCertPool
}
return config, nil
}
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
TLSConfig := &tls.Config{}

View File

@ -1,7 +1,13 @@
package handler
import (
"bytes"
"crypto/tls"
"strings"
"time"
"github.com/portainer/portainer"
"github.com/portainer/portainer/crypto"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
@ -56,15 +62,6 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag
}
type (
postEndpointsRequest struct {
Name string `valid:"required"`
URL string `valid:"required"`
PublicURL string `valid:"-"`
TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
}
postEndpointsResponse struct {
ID int `json:"Id"`
}
@ -82,6 +79,18 @@ type (
TLSSkipVerify bool `valid:"-"`
TLSSkipClientVerify bool `valid:"-"`
}
postEndpointPayload struct {
name string
url string
publicURL string
useTLS bool
skipTLSServerVerification bool
skipTLSClientVerification bool
caCert []byte
cert []byte
key []byte
}
)
// handleGetEndpoints handles GET requests on /endpoints
@ -107,32 +116,48 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt
encodeJSON(w, filteredEndpoints, handler.Logger)
}
// handlePostEndpoints handles POST requests on /endpoints
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
func sendPingRequest(host string, tlsConfig *tls.Config) error {
transport := &http.Transport{}
scheme := "http"
if tlsConfig != nil {
transport.TLSClientConfig = tlsConfig
scheme = "https"
}
var req postEndpointsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
client := &http.Client{
Timeout: time.Second * 3,
Transport: transport,
}
_, err := govalidator.ValidateStruct(req)
pingOperationURL := strings.Replace(host, "tcp://", scheme+"://", 1) + "/_ping"
_, err := client.Get(pingOperationURL)
if err != nil {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
return err
}
return nil
}
func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
tlsConfig, err := crypto.CreateTLSConfig(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification)
if err != nil {
return nil, err
}
err = sendPingRequest(payload.url, tlsConfig)
if err != nil {
return nil, err
}
endpoint := &portainer.Endpoint{
Name: req.Name,
URL: req.URL,
PublicURL: req.PublicURL,
Name: payload.name,
URL: payload.url,
PublicURL: payload.publicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: req.TLS,
TLSSkipVerify: req.TLSSkipVerify,
TLS: payload.useTLS,
TLSSkipVerify: payload.skipTLSServerVerification,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
@ -141,30 +166,147 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
err = handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return nil, err
}
folder := strconv.Itoa(int(endpoint.ID))
if !payload.skipTLSServerVerification {
r := bytes.NewReader(payload.caCert)
// TODO: review the API exposed by the FileService to store
// a file from a byte slice and return the path to the stored file instead
// of using multiple legacy calls (StoreTLSFile, GetPathForTLSFile) here.
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCA, r)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return nil, err
}
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
}
if !payload.skipTLSClientVerification {
r := bytes.NewReader(payload.cert)
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCert, r)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return nil, err
}
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
r = bytes.NewReader(payload.key)
err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileKey, r)
if err != nil {
handler.EndpointService.DeleteEndpoint(endpoint.ID)
return nil, err
}
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return nil, err
}
return endpoint, nil
}
func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
if !strings.HasPrefix(payload.url, "unix://") {
err := sendPingRequest(payload.url, nil)
if err != nil {
return nil, err
}
}
endpoint := &portainer.Endpoint{
Name: payload.name,
URL: payload.url,
PublicURL: payload.publicURL,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
}
err := handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, err
}
return endpoint, nil
}
func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) {
if payload.useTLS {
return handler.createTLSSecuredEndpoint(payload)
}
return handler.createUnsecuredEndpoint(payload)
}
func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload, error) {
payload := &postEndpointPayload{}
payload.name = r.FormValue("Name")
payload.url = r.FormValue("URL")
payload.publicURL = r.FormValue("PublicURL")
if payload.name == "" || payload.url == "" {
return nil, ErrInvalidRequestFormat
}
payload.useTLS = r.FormValue("TLS") == "true"
if payload.useTLS {
payload.skipTLSServerVerification = r.FormValue("TLSSkipVerify") == "true"
payload.skipTLSClientVerification = r.FormValue("TLSSkipClientVerify") == "true"
if !payload.skipTLSServerVerification {
caCert, err := getUploadedFileContent(r, "TLSCACertFile")
if err != nil {
return nil, err
}
payload.caCert = caCert
}
if !payload.skipTLSClientVerification {
cert, err := getUploadedFileContent(r, "TLSCertFile")
if err != nil {
return nil, err
}
payload.cert = cert
key, err := getUploadedFileContent(r, "TLSKeyFile")
if err != nil {
return nil, err
}
payload.key = key
}
}
return payload, nil
}
// handlePostEndpoints handles POST requests on /endpoints
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
if req.TLS {
folder := strconv.Itoa(int(endpoint.ID))
payload, err := convertPostEndpointRequestToPayload(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
return
}
if !req.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSConfig.TLSCACertPath = caCertPath
}
if !req.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
endpoint, err := handler.createEndpoint(payload)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)

View File

@ -2,6 +2,7 @@ package handler
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"strings"
@ -95,3 +96,19 @@ func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger)
}
}
// getUploadedFileContent retrieve the content of a file uploaded in the request.
// Uses requestParameter as the key to retrieve the file in the request payload.
func getUploadedFileContent(request *http.Request, requestParameter string) ([]byte, error) {
file, _, err := request.FormFile(requestParameter)
if err != nil {
return nil, err
}
defer file.Close()
fileContent, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
return fileContent, nil
}

View File

@ -84,7 +84,6 @@
{{ $ctrl.formData.TLSCACert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCACert && $ctrl.formData.TLSCACert === $ctrl.endpoint.TLSConfig.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
@ -100,7 +99,6 @@
{{ $ctrl.formData.TLSCert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCert && $ctrl.formData.TLSCert === $ctrl.endpoint.TLSConfig.TLSCert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
@ -114,7 +112,6 @@
{{ $ctrl.formData.TLSKey.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSKey && $ctrl.formData.TLSKey === $ctrl.endpoint.TLSConfig.TLSKey" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>

View File

@ -2,7 +2,6 @@ angular.module('portainer.app')
.factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:id/:action', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },

View File

@ -50,42 +50,28 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
};
service.createLocalEndpoint = function(name, URL, TLS, active) {
var endpoint = {
Name: 'local',
URL: 'unix:///var/run/docker.sock',
TLS: false
};
return Endpoints.create({}, endpoint).$promise;
var deferred = $q.defer();
FileUploadService.createEndpoint('local', 'unix:///var/run/docker.sock', '', false)
.then(function success(response) {
deferred.resolve(response.data);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to create endpoint', err: err});
});
return deferred.promise;
};
service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
var endpoint = {
Name: name,
URL: 'tcp://' + URL,
PublicURL: PublicURL,
TLS: TLS,
TLSSkipVerify: TLSSkipVerify,
TLSSkipClientVerify: TLSSkipClientVerify
};
var deferred = $q.defer();
Endpoints.create({}, endpoint).$promise
.then(function success(data) {
var endpointID = data.Id;
if (!TLSSkipVerify || !TLSSkipClientVerify) {
deferred.notify({upload: true});
FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success() {
deferred.notify({upload: false});
deferred.resolve(data);
});
} else {
deferred.resolve(data);
}
FileUploadService.createEndpoint(name, 'tcp://' + URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success(response) {
deferred.resolve(response.data);
})
.catch(function error(err) {
deferred.notify({upload: false});
deferred.reject({msg: 'Unable to upload TLS certs', err: err});
deferred.reject({msg: 'Unable to create endpoint', err: err});
});
return deferred.promise;

View File

@ -42,6 +42,24 @@ angular.module('portainer.app')
});
};
service.createEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
return Upload.upload({
url: 'api/endpoints',
data: {
Name: name,
URL: URL,
PublicURL: PublicURL,
TLS: TLS,
TLSSkipVerify: TLSSkipVerify,
TLSSkipClientVerify: TLSSkipClientVerify,
TLSCACertFile: TLSCAFile,
TLSCertFile: TLSCertFile,
TLSKeyFile: TLSKeyFile
},
ignoreLoadingBar: true
});
};
service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];

View File

@ -1,6 +1,6 @@
angular.module('portainer.app')
.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications', 'SystemService', 'EndpointProvider',
function ($scope, $state, $filter, EndpointService, Notifications, SystemService, EndpointProvider) {
.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications',
function ($scope, $state, $filter, EndpointService, Notifications) {
$scope.state = {
uploadInProgress: false,
actionInProgress: false
@ -30,34 +30,17 @@ function ($scope, $state, $filter, EndpointService, Notifications, SystemService
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
var endpointId;
$scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success(data) {
endpointId = data.Id;
var currentEndpointId = EndpointProvider.endpointID();
EndpointProvider.setEndpointID(endpointId);
SystemService.info()
.then(function success() {
Notifications.success('Endpoint created', name);
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create endpoint');
EndpointService.deleteEndpoint(endpointId);
})
.finally(function final() {
$scope.state.actionInProgress = false;
EndpointProvider.setEndpointID(currentEndpointId);
});
}, function error(err) {
$scope.state.uploadInProgress = false;
$scope.state.actionInProgress = false;
.then(function success() {
Notifications.success('Endpoint created', name);
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create endpoint');
}, function update(evt) {
if (evt.upload) {
$scope.state.uploadInProgress = evt.upload;
}
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};

View File

@ -46,7 +46,6 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
EndpointService.deleteEndpoint(endpointID);
})
.finally(function final() {
$scope.state.actionInProgress = false;
@ -81,7 +80,6 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
EndpointService.deleteEndpoint(endpointID);
})
.finally(function final() {
$scope.state.actionInProgress = false;