From a61654a35d87d0b0311569e745f808a9cf5d4e2c Mon Sep 17 00:00:00 2001 From: baron_l Date: Sun, 28 Oct 2018 10:27:06 +0100 Subject: [PATCH] feat(endpoints): add the ability to browse offline endpoints (#2253) * feat(back): saved data in snapshot * feat(endpoints): adding interceptors to retrieve saved data on offline endpoints * feat(endpoints): offline dashboard working - need tests on offline views * refactor(endpoints): interceptors cleaning and saving/loading offline endpoints data in/from localstorage * feat(endpoints): browsing offline endpoints * feat(endpoints): removing all the link in offline mode - sidebar not working when switching between off and on modes w/ stateManager logic * feat(endpoints): endpoint status detection in real time * fix(endpoints): offline swarm endpoint are not accessible anymore * fix(endpoints): refactor message + disable offline browsing for an endpoint when no snapshot is available for it * fix(endpoints): adding timeout and enabling loading bar for offline requests * fix(endpoints): trying to access a down endpoint wont remove sidebar items if it fails * feat(endpoints): disable checkboxes on offline views for offline mode * feat(endpoints): updating endpoint status when detecting a change * refactor(host): moved offline status panel from engine view to new host view * fix(endpoints): missing endpoint update on ping from home view * fix(api): rework EndpointUpdate operation * refactor(offline): moved endpoint status to EndpointProvider and refactor the status-changed detection * fix(offline): moved status detection to callback on views -> prevent displaying the offline message when endpoint is back online on view change * fix(offline): offline message is now displayed online when browsing an offline endpoint * fix(offline): sidebar updates correctly on endpoint status change * fix(offline): offline panel not displayed and hidden on online mode * refactor(offline): rework of OfflineMode management * refactor(offline): extract information-panel for offlineMode into a component * refactor(offline): remove redundant binding of informationPanel + endpointStatusInterceptor patter as service * refactor(interceptors): moved interceptors pattern to service pattern * feat(stacks): prevent inspection of a stack in offline mode * feat(host): hide devices/disk panels in offline mode * feat(host): disable browse action in offline mode * refactor(home): remove comments --- api/docker/snapshot.go | 32 +++++ api/http/handler/endpoints/endpoint_update.go | 135 +++++++++++------- api/portainer.go | 33 +++-- app/config.js | 2 + .../containersDatatable.html | 18 ++- .../containersDatatable.js | 3 +- .../containersDatatableController.js | 1 - .../images-datatable/imagesDatatable.html | 9 +- .../images-datatable/imagesDatatable.js | 3 +- .../networks-datatable/networksDatatable.html | 9 +- .../networks-datatable/networksDatatable.js | 3 +- .../volumes-datatable/volumesDatatable.html | 11 +- .../volumes-datatable/volumesDatatable.js | 3 +- .../docker-sidebar-content.js | 3 +- .../dockerSidebarContent.html | 4 +- .../host-overview/host-overview.html | 12 +- .../components/host-overview/host-overview.js | 1 + .../interceptors/containersInterceptor.js | 21 +++ app/docker/interceptors/imagesInterceptor.js | 21 +++ app/docker/interceptors/infoInterceptor.js | 21 +++ .../interceptors/networksInterceptor.js | 21 +++ app/docker/interceptors/versionInterceptor.js | 21 +++ app/docker/interceptors/volumesInterceptor.js | 21 +++ app/docker/rest/container.js | 6 +- app/docker/rest/image.js | 6 +- app/docker/rest/network.js | 6 +- app/docker/rest/system.js | 8 +- app/docker/rest/systemEndpoint.js | 13 ++ app/docker/rest/volume.js | 5 +- app/docker/services/systemService.js | 6 +- app/docker/views/containers/containers.html | 3 +- .../views/containers/containersController.js | 7 +- app/docker/views/dashboard/dashboard.html | 2 +- .../views/dashboard/dashboardController.js | 3 + app/docker/views/host/host-view-controller.js | 8 +- app/docker/views/host/host-view.html | 3 +- app/docker/views/images/images.html | 5 +- app/docker/views/images/imagesController.js | 7 +- app/docker/views/networks/networks.html | 3 +- .../views/networks/networksController.js | 7 +- app/docker/views/volumes/volumes.html | 3 +- app/docker/views/volumes/volumesController.js | 7 +- .../stacks-datatable/stacksDatatable.html | 9 +- .../stacks-datatable/stacksDatatable.js | 3 +- .../informationPanelOffline.html | 8 ++ .../informationPanelOffline.js | 4 + .../information-panel/information-panel.js | 2 +- .../information-panel/informationPanel.html | 2 +- .../interceptors/endpointStatusInterceptor.js | 41 ++++++ app/portainer/services/endpointProvider.js | 50 +++++++ app/portainer/services/extensionManager.js | 7 +- app/portainer/services/localStorage.js | 12 ++ app/portainer/services/stateManager.js | 18 +-- app/portainer/views/home/homeController.js | 106 ++++++++++---- app/portainer/views/main/mainController.js | 5 +- app/portainer/views/sidebar/sidebar.html | 1 + .../views/sidebar/sidebarController.js | 59 ++++---- app/portainer/views/stacks/stacks.html | 3 +- .../views/stacks/stacksController.js | 3 + 59 files changed, 637 insertions(+), 212 deletions(-) create mode 100644 app/docker/interceptors/containersInterceptor.js create mode 100644 app/docker/interceptors/imagesInterceptor.js create mode 100644 app/docker/interceptors/infoInterceptor.js create mode 100644 app/docker/interceptors/networksInterceptor.js create mode 100644 app/docker/interceptors/versionInterceptor.js create mode 100644 app/docker/interceptors/volumesInterceptor.js create mode 100644 app/docker/rest/systemEndpoint.js create mode 100644 app/portainer/components/information-panel-offline/informationPanelOffline.html create mode 100644 app/portainer/components/information-panel-offline/informationPanelOffline.js create mode 100644 app/portainer/interceptors/endpointStatusInterceptor.js diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index d8c481fb7..342465b1f 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -52,6 +52,16 @@ func snapshot(cli *client.Client) (*portainer.Snapshot, error) { return nil, err } + err = snapshotNetworks(snapshot, cli) + if err != nil { + return nil, err + } + + err = snapshotVersion(snapshot, cli) + if err != nil { + return nil, err + } + snapshot.Time = time.Now().Unix() return snapshot, nil } @@ -66,6 +76,7 @@ func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error { snapshot.DockerVersion = info.ServerVersion snapshot.TotalCPU = info.NCPU snapshot.TotalMemory = info.MemTotal + snapshot.SnapshotRaw.Info = info return nil } @@ -132,6 +143,7 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error snapshot.RunningContainerCount = runningContainers snapshot.StoppedContainerCount = stoppedContainers snapshot.StackCount += len(stacks) + snapshot.SnapshotRaw.Containers = containers return nil } @@ -142,6 +154,7 @@ func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error { } snapshot.ImageCount = len(images) + snapshot.SnapshotRaw.Images = images return nil } @@ -152,5 +165,24 @@ func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error { } snapshot.VolumeCount = len(volumes.Volumes) + snapshot.SnapshotRaw.Volumes = volumes + return nil +} + +func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error { + networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{}) + if err != nil { + return err + } + snapshot.SnapshotRaw.Networks = networks + return nil +} + +func snapshotVersion(snapshot *portainer.Snapshot, cli *client.Client) error { + version, err := cli.ServerVersion(context.Background()) + if err != nil { + return err + } + snapshot.SnapshotRaw.Version = version return nil } diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 5244cdc98..405f6ce38 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -12,16 +12,17 @@ import ( ) type endpointUpdatePayload struct { - Name string - URL string - PublicURL string - GroupID int - TLS bool - TLSSkipVerify bool - TLSSkipClientVerify bool - AzureApplicationID string - AzureTenantID string - AzureAuthenticationKey string + Name *string + URL *string + PublicURL *string + GroupID *int + TLS *bool + TLSSkipVerify *bool + TLSSkipClientVerify *bool + Status *int + AzureApplicationID *string + AzureTenantID *string + AzureAuthenticationKey *string Tags []string } @@ -53,36 +54,49 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - if payload.Name != "" { - endpoint.Name = payload.Name + if payload.Name != nil { + endpoint.Name = *payload.Name } - if payload.URL != "" { - endpoint.URL = payload.URL + if payload.URL != nil { + endpoint.URL = *payload.URL } - if payload.PublicURL != "" { - endpoint.PublicURL = payload.PublicURL + if payload.PublicURL != nil { + endpoint.PublicURL = *payload.PublicURL } - if payload.GroupID != 0 { - endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID) + if payload.GroupID != nil { + endpoint.GroupID = portainer.EndpointGroupID(*payload.GroupID) } if payload.Tags != nil { endpoint.Tags = payload.Tags } + if payload.Status != nil { + switch *payload.Status { + case 1: + endpoint.Status = portainer.EndpointStatusUp + break + case 2: + endpoint.Status = portainer.EndpointStatusDown + break + default: + break + } + } + if endpoint.Type == portainer.AzureEnvironment { credentials := endpoint.AzureCredentials - if payload.AzureApplicationID != "" { - credentials.ApplicationID = payload.AzureApplicationID + if payload.AzureApplicationID != nil { + credentials.ApplicationID = *payload.AzureApplicationID } - if payload.AzureTenantID != "" { - credentials.TenantID = payload.AzureTenantID + if payload.AzureTenantID != nil { + credentials.TenantID = *payload.AzureTenantID } - if payload.AzureAuthenticationKey != "" { - credentials.AuthenticationKey = payload.AzureAuthenticationKey + if payload.AzureAuthenticationKey != nil { + credentials.AuthenticationKey = *payload.AzureAuthenticationKey } httpClient := client.NewHTTPClient() @@ -93,44 +107,55 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint.AzureCredentials = credentials } - folder := strconv.Itoa(endpointID) - if payload.TLS { - endpoint.TLSConfig.TLS = true - endpoint.TLSConfig.TLSSkipVerify = payload.TLSSkipVerify - if !payload.TLSSkipVerify { - caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) - endpoint.TLSConfig.TLSCACertPath = caCertPath - } else { - endpoint.TLSConfig.TLSCACertPath = "" - handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA) - } + if payload.TLS != nil { + folder := strconv.Itoa(endpointID) + + if *payload.TLS { + endpoint.TLSConfig.TLS = true + if payload.TLSSkipVerify != nil { + endpoint.TLSConfig.TLSSkipVerify = *payload.TLSSkipVerify + + if !*payload.TLSSkipVerify { + caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) + endpoint.TLSConfig.TLSCACertPath = caCertPath + } else { + endpoint.TLSConfig.TLSCACertPath = "" + handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA) + } + } + + if payload.TLSSkipClientVerify != nil { + if !*payload.TLSSkipClientVerify { + certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) + endpoint.TLSConfig.TLSCertPath = certPath + keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) + endpoint.TLSConfig.TLSKeyPath = keyPath + } else { + endpoint.TLSConfig.TLSCertPath = "" + handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert) + endpoint.TLSConfig.TLSKeyPath = "" + handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey) + } + } - if !payload.TLSSkipClientVerify { - certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) - endpoint.TLSConfig.TLSCertPath = certPath - keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) - endpoint.TLSConfig.TLSKeyPath = keyPath } else { + endpoint.TLSConfig.TLS = false + endpoint.TLSConfig.TLSSkipVerify = false + endpoint.TLSConfig.TLSCACertPath = "" endpoint.TLSConfig.TLSCertPath = "" - handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert) endpoint.TLSConfig.TLSKeyPath = "" - handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey) - } - } else { - endpoint.TLSConfig.TLS = false - endpoint.TLSConfig.TLSSkipVerify = false - endpoint.TLSConfig.TLSCACertPath = "" - endpoint.TLSConfig.TLSCertPath = "" - endpoint.TLSConfig.TLSKeyPath = "" - err = handler.FileService.DeleteTLSFiles(folder) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err} + err = handler.FileService.DeleteTLSFiles(folder) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err} + } } } - _, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err} + if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment { + _, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err} + } } err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) diff --git a/api/portainer.go b/api/portainer.go index c1cdbab4f..2c866f3da 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -245,17 +245,28 @@ type ( // Snapshot represents a snapshot of a specific endpoint at a specific time Snapshot struct { - Time int64 `json:"Time"` - DockerVersion string `json:"DockerVersion"` - Swarm bool `json:"Swarm"` - TotalCPU int `json:"TotalCPU"` - TotalMemory int64 `json:"TotalMemory"` - RunningContainerCount int `json:"RunningContainerCount"` - StoppedContainerCount int `json:"StoppedContainerCount"` - VolumeCount int `json:"VolumeCount"` - ImageCount int `json:"ImageCount"` - ServiceCount int `json:"ServiceCount"` - StackCount int `json:"StackCount"` + Time int64 `json:"Time"` + DockerVersion string `json:"DockerVersion"` + Swarm bool `json:"Swarm"` + TotalCPU int `json:"TotalCPU"` + TotalMemory int64 `json:"TotalMemory"` + RunningContainerCount int `json:"RunningContainerCount"` + StoppedContainerCount int `json:"StoppedContainerCount"` + VolumeCount int `json:"VolumeCount"` + ImageCount int `json:"ImageCount"` + ServiceCount int `json:"ServiceCount"` + StackCount int `json:"StackCount"` + SnapshotRaw SnapshotRaw `json:"SnapshotRaw"` + } + + // SnapshotRaw represents all the information related to a snapshot as returned by the Docker API + SnapshotRaw struct { + Containers interface{} `json:"Containers"` + Volumes interface{} `json:"Volumes"` + Networks interface{} `json:"Networks"` + Images interface{} `json:"Images"` + Info interface{} `json:"Info"` + Version interface{} `json:"Version"` } // EndpointGroupID represents an endpoint group identifier diff --git a/app/config.js b/app/config.js index 4cdd7fa8c..cd21e1f08 100644 --- a/app/config.js +++ b/app/config.js @@ -17,6 +17,7 @@ angular.module('portainer') }] }); $httpProvider.interceptors.push('jwtInterceptor'); + $httpProvider.interceptors.push('EndpointStatusInterceptor'); $httpProvider.defaults.headers.post['Content-Type'] = 'application/json'; $httpProvider.defaults.headers.put['Content-Type'] = 'application/json'; $httpProvider.defaults.headers.patch['Content-Type'] = 'application/json'; @@ -48,6 +49,7 @@ angular.module('portainer') cfpLoadingBarProvider.includeSpinner = false; cfpLoadingBarProvider.parentSelector = '#loadingbar-placeholder'; + cfpLoadingBarProvider.latencyThreshold = 600; $urlRouterProvider.otherwise('/auth'); }]); diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 19cd2a0c0..5b30f1473 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -99,7 +99,7 @@ - - + @@ -210,17 +210,18 @@ - + - {{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }} + {{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }} + {{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }} {{ item.Status }} {{ item.Status }} - +
@@ -228,8 +229,13 @@
+ + {{ item.StackName ? item.StackName : '-' }} - {{ item.Image | trimshasum }} + + {{ item.Image | trimshasum }} + {{ item.Image | trimshasum }} + {{item.Created | getisodatefromtimestamp}} diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.js b/app/docker/components/datatables/containers-datatable/containersDatatable.js index 18a693511..478f17367 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.js +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.js @@ -10,6 +10,7 @@ angular.module('portainer.docker').component('containersDatatable', { reverseOrder: '<', showOwnershipColumn: '<', showHostColumn: '<', - showAddAction: '<' + showAddAction: '<', + offlineMode: '<' } }); diff --git a/app/docker/components/datatables/containers-datatable/containersDatatableController.js b/app/docker/components/datatables/containers-datatable/containersDatatableController.js index 7151e5d64..2fd6ffe1e 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatableController.js +++ b/app/docker/components/datatables/containers-datatable/containersDatatableController.js @@ -204,7 +204,6 @@ function (PaginationService, DatatableService, EndpointProvider) { this.$onInit = function() { setDefaults(this); this.prepareTableFromDataset(); - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); if (storedOrder !== null) { this.state.reverseOrder = storedOrder.reverse; diff --git a/app/docker/components/datatables/images-datatable/imagesDatatable.html b/app/docker/components/datatables/images-datatable/imagesDatatable.html index 87cba5507..a509ebd75 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatable.html +++ b/app/docker/components/datatables/images-datatable/imagesDatatable.html @@ -6,7 +6,7 @@ {{ $ctrl.titleText }} -
+
-
+
-
+
- + 1) { diff --git a/app/docker/views/host/host-view.html b/app/docker/views/host/host-view.html index 35a4f513e..61597ecca 100644 --- a/app/docker/views/host/host-view.html +++ b/app/docker/views/host/host-view.html @@ -7,7 +7,8 @@ devices="$ctrl.devices" refresh-url="docker.host" browse-url="docker.host.browser" - is-job-enabled="$ctrl.state.isAdmin" + offline-mode="$ctrl.state.offlineMode" + is-job-enabled="$ctrl.state.isAdmin && !$ctrl.state.offlineMode" job-url="docker.host.job" jobs="$ctrl.jobs" > diff --git a/app/docker/views/images/images.html b/app/docker/views/images/images.html index 3c8df020a..8637e4e25 100644 --- a/app/docker/views/images/images.html +++ b/app/docker/views/images/images.html @@ -7,7 +7,7 @@ Images -
+
@@ -51,7 +51,7 @@
- +
diff --git a/app/docker/views/images/imagesController.js b/app/docker/views/images/imagesController.js index f495f4185..0504ceaea 100644 --- a/app/docker/views/images/imagesController.js +++ b/app/docker/views/images/imagesController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', 'FileSaver', 'Blob', -function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob) { +.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', 'FileSaver', 'Blob', 'EndpointProvider', +function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider) { $scope.state = { actionInProgress: false, exportInProgress: false @@ -113,10 +113,13 @@ function ($scope, $state, ImageService, Notifications, ModalService, HttpRequest }); }; + $scope.offlineMode = false; + function initView() { ImageService.images(true) .then(function success(data) { $scope.images = data; + $scope.offlineMode = EndpointProvider.offlineMode(); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve images'); diff --git a/app/docker/views/networks/networks.html b/app/docker/views/networks/networks.html index ff0f6edd6..c547d54ba 100644 --- a/app/docker/views/networks/networks.html +++ b/app/docker/views/networks/networks.html @@ -6,7 +6,7 @@ Networks - +
diff --git a/app/docker/views/networks/networksController.js b/app/docker/views/networks/networksController.js index c730dc337..77c7647d9 100644 --- a/app/docker/views/networks/networksController.js +++ b/app/docker/views/networks/networksController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('NetworksController', ['$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper', -function ($scope, $state, NetworkService, Notifications, HttpRequestHelper) { +.controller('NetworksController', ['$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper', 'EndpointProvider', +function ($scope, $state, NetworkService, Notifications, HttpRequestHelper, EndpointProvider) { $scope.removeAction = function (selectedItems) { var actionCount = selectedItems.length; @@ -24,10 +24,13 @@ function ($scope, $state, NetworkService, Notifications, HttpRequestHelper) { }); }; + $scope.offlineMode = false; + function initView() { NetworkService.networks(true, true, true) .then(function success(data) { $scope.networks = data; + $scope.offlineMode = EndpointProvider.offlineMode(); }) .catch(function error(err) { $scope.networks = []; diff --git a/app/docker/views/volumes/volumes.html b/app/docker/views/volumes/volumes.html index 072394f45..958246e49 100644 --- a/app/docker/views/volumes/volumes.html +++ b/app/docker/views/volumes/volumes.html @@ -6,7 +6,7 @@ Volumes - +
diff --git a/app/docker/views/volumes/volumesController.js b/app/docker/views/volumes/volumesController.js index 362c54b4b..3ab620266 100644 --- a/app/docker/views/volumes/volumesController.js +++ b/app/docker/views/volumes/volumesController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('VolumesController', ['$q', '$scope', '$state', 'VolumeService', 'ServiceService', 'VolumeHelper', 'Notifications', 'HttpRequestHelper', -function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper) { +.controller('VolumesController', ['$q', '$scope', '$state', 'VolumeService', 'ServiceService', 'VolumeHelper', 'Notifications', 'HttpRequestHelper', 'EndpointProvider', +function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider) { $scope.removeAction = function (selectedItems) { var actionCount = selectedItems.length; @@ -24,6 +24,8 @@ function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notif }); }; + $scope.offlineMode = false; + function initView() { var endpointProvider = $scope.applicationState.endpoint.mode.provider; var endpointRole = $scope.applicationState.endpoint.mode.role; @@ -35,6 +37,7 @@ function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notif }) .then(function success(data) { var services = data.services; + $scope.offlineMode = EndpointProvider.offlineMode(); $scope.volumes = data.attached.map(function(volume) { volume.dangling = false; return volume; diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index 5b640ea17..b91a8e1da 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -6,7 +6,7 @@ {{ $ctrl.titleText }}
-
+
diff --git a/app/portainer/interceptors/endpointStatusInterceptor.js b/app/portainer/interceptors/endpointStatusInterceptor.js new file mode 100644 index 000000000..46ca7a42b --- /dev/null +++ b/app/portainer/interceptors/endpointStatusInterceptor.js @@ -0,0 +1,41 @@ +angular.module('portainer.app') + .factory('EndpointStatusInterceptor', ['$q', '$injector', 'EndpointProvider', function ($q, $injector, EndpointProvider) { + 'use strict'; + var interceptor = {}; + + interceptor.response = responseInterceptor; + interceptor.responseError = responseErrorInterceptor; + + function canBeOffline(url) { + return (_.startsWith(url, 'api/') && ( + _.includes(url, '/containers') || + _.includes(url, '/images') || + _.includes(url, '/volumes') || + _.includes(url, '/networks') || + _.includes(url, '/info') || + _.includes(url, '/version') + )); + } + + function responseInterceptor(response) { + var EndpointService = $injector.get('EndpointService'); + var url = response.config.url; + if (response.status === 200 && canBeOffline(url) && EndpointProvider.offlineMode()) { + EndpointProvider.setOfflineMode(false); + EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(false)}); + } + return response || $q.when(response); + } + + function responseErrorInterceptor(rejection) { + var EndpointService = $injector.get('EndpointService'); + var url = rejection.config.url; + if ((rejection.status === 502 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) { + EndpointProvider.setOfflineMode(true); + EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(true)}); + } + return $q.reject(rejection); + } + + return interceptor; + }]); \ No newline at end of file diff --git a/app/portainer/services/endpointProvider.js b/app/portainer/services/endpointProvider.js index 21d635d64..ebd89497e 100644 --- a/app/portainer/services/endpointProvider.js +++ b/app/portainer/services/endpointProvider.js @@ -7,19 +7,31 @@ angular.module('portainer.app') service.initialize = function() { var endpointID = LocalStorage.getEndpointID(); var endpointPublicURL = LocalStorage.getEndpointPublicURL(); + var offlineMode = LocalStorage.getOfflineMode(); + if (endpointID) { endpoint.ID = endpointID; } if (endpointPublicURL) { endpoint.PublicURL = endpointPublicURL; } + if (offlineMode) { + endpoint.OfflineMode = offlineMode; + } }; service.clean = function() { endpoint = {}; }; + service.endpoint = function() { + return endpoint; + }; + service.endpointID = function() { + if (endpoint.ID === undefined) { + endpoint.ID = LocalStorage.getEndpointID(); + } return endpoint.ID; }; @@ -29,6 +41,9 @@ angular.module('portainer.app') }; service.endpointPublicURL = function() { + if (endpoint.PublicURL === undefined) { + endpoint.PublicURL = LocalStorage.getEndpointPublicURL(); + } return endpoint.PublicURL; }; @@ -37,5 +52,40 @@ angular.module('portainer.app') LocalStorage.storeEndpointPublicURL(publicURL); }; + service.endpoints = function() { + return LocalStorage.getEndpoints(); + }; + + service.setEndpoints = function(data) { + LocalStorage.storeEndpoints(data); + }; + + service.offlineMode = function() { + return endpoint.OfflineMode; + }; + + service.endpointStatusFromOfflineMode = function(isOffline) { + return isOffline ? 2 : 1; + }; + + service.setOfflineMode = function(isOffline) { + endpoint.OfflineMode = isOffline; + LocalStorage.storeOfflineMode(isOffline); + }; + + service.setOfflineModeFromStatus = function(status) { + var isOffline = status !== 1; + endpoint.OfflineMode = isOffline; + LocalStorage.storeOfflineMode(isOffline); + }; + + service.currentEndpoint = function() { + var endpointId = endpoint.ID; + var endpoints = LocalStorage.getEndpoints(); + return _.find(endpoints, function (item) { + return item.Id === endpointId; + }); + }; + return service; }]); diff --git a/app/portainer/services/extensionManager.js b/app/portainer/services/extensionManager.js index b0cde467e..e9f622746 100644 --- a/app/portainer/services/extensionManager.js +++ b/app/portainer/services/extensionManager.js @@ -4,9 +4,14 @@ function ExtensionManagerFactory($q, PluginService, SystemService, ExtensionServ 'use strict'; var service = {}; - service.initEndpointExtensions = function() { + service.initEndpointExtensions = function(endpoint) { var deferred = $q.defer(); + if (endpoint.Status !== 1) { + deferred.resolve([]); + return deferred.promise; + } + SystemService.version() .then(function success(data) { var endpointAPIVersion = parseFloat(data.ApiVersion); diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index a676b33a4..2988ebd9d 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -14,6 +14,18 @@ angular.module('portainer.app') getEndpointPublicURL: function() { return localStorageService.get('ENDPOINT_PUBLIC_URL'); }, + storeOfflineMode: function(isOffline) { + localStorageService.set('ENDPOINT_OFFLINE_MODE', isOffline); + }, + getOfflineMode: function() { + return localStorageService.get('ENDPOINT_OFFLINE_MODE'); + }, + storeEndpoints: function(data) { + localStorageService.set('ENDPOINTS_DATA', data); + }, + getEndpoints: function() { + return localStorageService.get('ENDPOINTS_DATA'); + }, storeEndpointState: function(state) { localStorageService.set('ENDPOINT_STATE', state); }, diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 36c7dadb5..987022476 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -135,11 +135,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin return extensions; } - manager.updateEndpointState = function(name, type, extensions) { + manager.updateEndpointState = function(endpoint, extensions) { var deferred = $q.defer(); - if (type === 3) { - state.endpoint.name = name; + if (endpoint.Type === 3) { + state.endpoint.name = endpoint.Name; state.endpoint.mode = { provider: 'AZURE' }; LocalStorage.storeEndpointState(state.endpoint); deferred.resolve(); @@ -147,23 +147,23 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin } $q.all({ - version: SystemService.version(), - info: SystemService.info() + version: endpoint.Status === 1 ? SystemService.version() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Version), + info: endpoint.Status === 1 ? SystemService.info() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Info) }) .then(function success(data) { - var endpointMode = InfoHelper.determineEndpointMode(data.info, type); + var endpointMode = InfoHelper.determineEndpointMode(data.info, endpoint.Type); var endpointAPIVersion = parseFloat(data.version.ApiVersion); state.endpoint.mode = endpointMode; - state.endpoint.name = name; + state.endpoint.name = endpoint.Name; state.endpoint.apiVersion = endpointAPIVersion; state.endpoint.extensions = assignExtensions(extensions); - if (endpointMode.agentProxy) { + if (endpointMode.agentProxy && endpoint.Status === 1) { return AgentPingService.ping().then(function onPingSuccess(data) { state.endpoint.agentApiVersion = data.version; }); } - + }).then(function () { LocalStorage.storeEndpointState(state.endpoint); deferred.resolve(); diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index f666e49d7..083c75625 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -1,44 +1,71 @@ angular.module('portainer.app') -.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', 'MotdService', -function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService, MotdService) { +.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', 'MotdService', 'SystemService', +function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService, MotdService, SystemService) { - $scope.goToDashboard = function(endpoint) { - EndpointProvider.setEndpointID(endpoint.Id); - EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); - if (endpoint.Type === 3) { - switchToAzureEndpoint(endpoint); - } else { - switchToDockerEndpoint(endpoint); - } + $scope.goToEdit = function(id) { + $state.go('portainer.endpoints.endpoint', { id: id }); }; - $scope.dismissImportantInformation = function(hash) { + $scope.goToDashboard = function (endpoint) { + if (endpoint.Type === 3) { + return switchToAzureEndpoint(endpoint); + } + + checkEndpointStatus(endpoint) + .then(function sucess() { + return switchToDockerEndpoint(endpoint); + }).catch(function error(err) { + Notifications.error('Failure', err, 'Unable to verify endpoint status'); + }); + }; + + $scope.dismissImportantInformation = function (hash) { StateManager.dismissImportantInformation(hash); }; - $scope.dismissInformationPanel = function(id) { + $scope.dismissInformationPanel = function (id) { StateManager.dismissInformationPanel(id); }; - function triggerSnapshot() { - EndpointService.snapshot() - .then(function success() { - Notifications.success('Success', 'Endpoints updated'); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'An error occured during endpoint snapshot'); - }); - } - - $scope.triggerSnapshot = function() { + $scope.triggerSnapshot = function () { ModalService.confirmEndpointSnapshot(function (result) { - if(!result) { return; } + if (!result) { + return; + } triggerSnapshot(); }); }; + function checkEndpointStatus(endpoint) { + var deferred = $q.defer(); + + var status = 1; + SystemService.ping(endpoint.Id) + .then(function sucess() { + status = 1; + }).catch(function error() { + status = 2; + }).finally(function () { + if (endpoint.Status === status) { + deferred.resolve(endpoint); + return deferred.promise; + } + + EndpointService.updateEndpoint(endpoint.Id, { Status: status }) + .then(function sucess() { + deferred.resolve(endpoint); + }).catch(function error(err) { + deferred.reject({msg: 'Unable to update endpoint status', err: err}); + }); + }); + + return deferred.promise; + } + function switchToAzureEndpoint(endpoint) { + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); StateManager.updateEndpointState(endpoint.Name, endpoint.Type, []) .then(function success() { $state.go('azure.dashboard'); @@ -49,10 +76,21 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G } function switchToDockerEndpoint(endpoint) { - ExtensionManager.initEndpointExtensions(endpoint.Id) + if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) { + Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.'); + return; + } else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) { + Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.'); + return; + } + + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); + ExtensionManager.initEndpointExtensions(endpoint) .then(function success(data) { var extensions = data; - return StateManager.updateEndpointState(endpoint.Name, endpoint.Type, extensions); + return StateManager.updateEndpointState(endpoint, extensions); }) .then(function success() { $state.go('docker.dashboard'); @@ -62,10 +100,15 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G }); } - $scope.goToEdit = goToEdit; - - function goToEdit(id) { - $state.go('portainer.endpoints.endpoint', { id: id }); + function triggerSnapshot() { + EndpointService.snapshot() + .then(function success() { + Notifications.success('Success', 'Endpoints updated'); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'An error occured during endpoint snapshot'); + }); } function initView() { @@ -85,6 +128,7 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G var groups = data.groups; EndpointHelper.mapGroupNameToEndpoint(endpoints, groups); $scope.endpoints = endpoints; + EndpointProvider.setEndpoints(endpoints); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); diff --git a/app/portainer/views/main/mainController.js b/app/portainer/views/main/mainController.js index 3eb3e9cde..d6c76f982 100644 --- a/app/portainer/views/main/mainController.js +++ b/app/portainer/views/main/mainController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('MainController', ['$scope', '$cookieStore', 'StateManager', -function ($scope, $cookieStore, StateManager) { +.controller('MainController', ['$scope', '$cookieStore', 'StateManager', 'EndpointProvider', +function ($scope, $cookieStore, StateManager, EndpointProvider) { /** * Sidebar Toggle & Cookie Control @@ -11,6 +11,7 @@ function ($scope, $cookieStore, StateManager) { }; $scope.applicationState = StateManager.getState(); + $scope.endpointState = EndpointProvider.endpoint(); $scope.$watch($scope.getWidth, function(newValue) { if (newValue >= mobileView) { diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index d105549c5..af042c575 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -22,6 +22,7 @@ swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'" standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'" admin-access="!applicationState.application.authentication || isAdmin" + offline-mode="endpointState.OfflineMode" >