mirror of https://github.com/portainer/portainer
feat(api): ping the endpoint at creation time (#1817)
parent
031b428e0c
commit
05d6abf57b
|
@ -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{}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' } },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue