diff --git a/api/http/handler/status/handler.go b/api/http/handler/status/handler.go index cb30c8479..00ed6b799 100644 --- a/api/http/handler/status/handler.go +++ b/api/http/handler/status/handler.go @@ -23,6 +23,8 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Han } h.Handle("/status", bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet) + h.Handle("/status/version", + bouncer.RestrictedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/status/status_inspect_version.go b/api/http/handler/status/status_inspect_version.go new file mode 100644 index 000000000..054bd670e --- /dev/null +++ b/api/http/handler/status/status_inspect_version.go @@ -0,0 +1,51 @@ +package status + +import ( + "encoding/json" + "net/http" + + "github.com/coreos/go-semver/semver" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" + + "github.com/portainer/libhttp/response" +) + +type inspectVersionResponse struct { + UpdateAvailable bool `json:"UpdateAvailable"` + LatestVersion string `json:"LatestVersion"` +} + +type githubData struct { + TagName string `json:"tag_name"` +} + +// GET request on /api/status/version +func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) { + motd, err := client.Get(portainer.VersionCheckURL, 5) + if err != nil { + response.JSON(w, &inspectVersionResponse{UpdateAvailable: false}) + return + } + + var data githubData + err = json.Unmarshal(motd, &data) + if err != nil { + response.JSON(w, &inspectVersionResponse{UpdateAvailable: false}) + return + } + + resp := inspectVersionResponse{ + UpdateAvailable: false, + } + + currentVersion := semver.New(portainer.APIVersion) + latestVersion := semver.New(data.TagName) + if currentVersion.LessThan(*latestVersion) { + resp.UpdateAvailable = true + resp.LatestVersion = data.TagName + } + + response.JSON(w, &resp) +} diff --git a/api/portainer.go b/api/portainer.go index 438002d23..d5d546bf5 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -908,6 +908,8 @@ const ( AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved MessageOfTheDayURL = AssetsServerURL + "/motd.json" + // VersionCheckURL represents the URL used to retrieve the latest version of Portainer + VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest" // ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.0.json" // PortainerAgentHeader represents the name of the header available in any agent response diff --git a/app/portainer/models/status.js b/app/portainer/models/status.js index 713d4f1fe..d91ae9baf 100644 --- a/app/portainer/models/status.js +++ b/app/portainer/models/status.js @@ -5,3 +5,8 @@ export function StatusViewModel(data) { this.Analytics = data.Analytics; this.Version = data.Version; } + +export function StatusVersionViewModel(data) { + this.UpdateAvailable = data.UpdateAvailable; + this.LatestVersion = data.LatestVersion; +} \ No newline at end of file diff --git a/app/portainer/rest/status.js b/app/portainer/rest/status.js index 888a948bf..9a9e55123 100644 --- a/app/portainer/rest/status.js +++ b/app/portainer/rest/status.js @@ -1,7 +1,8 @@ angular.module('portainer.app') .factory('Status', ['$resource', 'API_ENDPOINT_STATUS', function StatusFactory($resource, API_ENDPOINT_STATUS) { 'use strict'; - return $resource(API_ENDPOINT_STATUS, {}, { - get: { method: 'GET' } + return $resource(API_ENDPOINT_STATUS + '/:action', {}, { + get: { method: 'GET' }, + version: { method: 'GET', params: { action: 'version' } } }); }]); diff --git a/app/portainer/services/api/statusService.js b/app/portainer/services/api/statusService.js index eb7c1e343..05b5f87a1 100644 --- a/app/portainer/services/api/statusService.js +++ b/app/portainer/services/api/statusService.js @@ -1,4 +1,4 @@ -import { StatusViewModel } from "../../models/status"; +import {StatusVersionViewModel, StatusViewModel} from '../../models/status'; angular.module('portainer.app') .factory('StatusService', ['$q', 'Status', function StatusServiceFactory($q, Status) { @@ -20,5 +20,20 @@ angular.module('portainer.app') return deferred.promise; }; + service.version = function() { + var deferred = $q.defer(); + + Status.version().$promise + .then(function success(data) { + var status = new StatusVersionViewModel(data); + deferred.resolve(status); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve application version info', err: err }); + }); + + return deferred.promise; + }; + return service; }]); diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 69ceecb0c..070d32e72 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -19,6 +19,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin extensions: [] }; + manager.setVersionInfo = function(versionInfo) { + state.application.versionStatus = versionInfo; + LocalStorage.storeApplicationState(state.application); + }; + manager.dismissInformationPanel = function(id) { state.UI.dismissedInfoPanels[id] = true; LocalStorage.storeUIState(state.UI); diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 76d6b00f1..27a625785 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -3,7 +3,7 @@ import uuidv4 from 'uuid/v4'; class AuthenticationController { /* @ngInject */ - constructor($async, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, ExtensionService, StateManager, Notifications, SettingsService, URLHelper, LocalStorage) { + constructor($async, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, ExtensionService, StateManager, Notifications, SettingsService, URLHelper, LocalStorage, StatusService) { this.$async = $async; this.$scope = $scope; this.$state = $state; @@ -18,6 +18,7 @@ class AuthenticationController { this.SettingsService = SettingsService; this.URLHelper = URLHelper; this.LocalStorage = LocalStorage; + this.StatusService = StatusService; this.logo = this.StateManager.getState().application.logo; this.formValues = { @@ -33,6 +34,7 @@ class AuthenticationController { this.retrieveAndSaveEnabledExtensionsAsync = this.retrieveAndSaveEnabledExtensionsAsync.bind(this); this.retrievePermissionsAsync = this.retrievePermissionsAsync.bind(this); this.checkForEndpointsAsync = this.checkForEndpointsAsync.bind(this); + this.checkForLatestVersionAsync = this.checkForLatestVersionAsync.bind(this); this.postLoginSteps = this.postLoginSteps.bind(this); this.oAuthLoginAsync = this.oAuthLoginAsync.bind(this); @@ -134,10 +136,28 @@ class AuthenticationController { } } + async checkForLatestVersionAsync() { + let versionInfo = { + UpdateAvailable: false, + LatestVersion: '' + }; + + try { + const versionStatus = await this.StatusService.version(); + if (versionStatus.UpdateAvailable) { + versionInfo.UpdateAvailable = true; + versionInfo.LatestVersion = versionStatus.LatestVersion; + } + } finally { + this.StateManager.setVersionInfo(versionInfo); + } + } + async postLoginSteps() { await this.retrievePermissionsAsync(); await this.retrieveAndSaveEnabledExtensionsAsync(); await this.checkForEndpointsAsync(false); + await this.checkForLatestVersionAsync(); } /** * END POST LOGIN STEPS SECTION diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index fcc624920..a31864305 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -82,8 +82,14 @@ diff --git a/assets/css/app.css b/assets/css/app.css index 44ce2625d..543eed1f7 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -396,6 +396,14 @@ ul.sidebar .sidebar-list a.active { margin: 2px 0 2px 20px; } +.sidebar-footer-content .update-notification { + font-size: 14px; + padding: 12px; + border-radius: 2px; + background-color: #FF851B; + margin-bottom: 5px; +} + .sidebar-footer-content .version { font-size: 11px; margin: 11px 20px 0 7px;