From 601ae9daf209bd5d682532d6275072ffc27057a1 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 20 Sep 2017 20:58:09 +0200 Subject: [PATCH 01/20] fix(ldap): prevent panic if search error arise (#1216) --- api/ldap/ldap.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index 8a280f754..3ad222d05 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -33,7 +33,10 @@ func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearc // Deliberately skip errors on the search request so that we can jump to other search settings // if any issue arise with the current one. - sr, _ := conn.Search(searchRequest) + sr, err := conn.Search(searchRequest) + if err != nil { + continue + } if len(sr.Entries) == 1 { found = true From 819d0f6a160627c7c7d89f8f7c658b7297bc9be2 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 21 Sep 2017 10:23:51 +0200 Subject: [PATCH 02/20] refactor(app): split app.js in multiple files (#1217) --- app/__module.js | 65 +++ app/app.js | 837 ++------------------------------------- app/config.js | 37 ++ app/constants.js | 14 + app/filters/__module.js | 1 + app/helpers/__module.js | 1 + app/rest/__module.js | 1 + app/routes.js | 662 +++++++++++++++++++++++++++++++ app/services/__module.js | 1 + gruntfile.js | 2 +- 10 files changed, 818 insertions(+), 803 deletions(-) create mode 100644 app/__module.js create mode 100644 app/config.js create mode 100644 app/constants.js create mode 100644 app/filters/__module.js create mode 100644 app/helpers/__module.js create mode 100644 app/rest/__module.js create mode 100644 app/routes.js create mode 100644 app/services/__module.js diff --git a/app/__module.js b/app/__module.js new file mode 100644 index 000000000..869198ab0 --- /dev/null +++ b/app/__module.js @@ -0,0 +1,65 @@ +angular.module('portainer', [ + 'ui.bootstrap', + 'ui.router', + 'isteven-multi-select', + 'ngCookies', + 'ngSanitize', + 'ngFileUpload', + 'angularUtils.directives.dirPagination', + 'LocalStorageModule', + 'angular-jwt', + 'angular-google-analytics', + 'portainer.templates', + 'portainer.filters', + 'portainer.rest', + 'portainer.helpers', + 'portainer.services', + 'auth', + 'dashboard', + 'container', + 'containerConsole', + 'containerLogs', + 'containerStats', + 'serviceLogs', + 'containers', + 'createContainer', + 'createNetwork', + 'createRegistry', + 'createSecret', + 'createService', + 'createVolume', + 'engine', + 'endpoint', + 'endpointAccess', + 'endpoints', + 'events', + 'image', + 'images', + 'initAdmin', + 'initEndpoint', + 'main', + 'network', + 'networks', + 'node', + 'registries', + 'registry', + 'registryAccess', + 'secrets', + 'secret', + 'service', + 'services', + 'settings', + 'settingsAuthentication', + 'sidebar', + 'swarm', + 'swarmVisualizer', + 'task', + 'team', + 'teams', + 'templates', + 'user', + 'users', + 'userSettings', + 'volume', + 'volumes', + 'rzModule']); diff --git a/app/app.js b/app/app.js index 02754f6c8..21fa6ce67 100644 --- a/app/app.js +++ b/app/app.js @@ -1,808 +1,41 @@ -angular.module('portainer.filters', []); -angular.module('portainer.rest', ['ngResource']); -angular.module('portainer.services', []); -angular.module('portainer.helpers', []); -angular.module('portainer', [ - 'ui.bootstrap', - 'ui.router', - 'isteven-multi-select', - 'ngCookies', - 'ngSanitize', - 'ngFileUpload', - 'angularUtils.directives.dirPagination', - 'LocalStorageModule', - 'angular-jwt', - 'angular-google-analytics', - 'portainer.templates', - 'portainer.filters', - 'portainer.rest', - 'portainer.helpers', - 'portainer.services', - 'auth', - 'dashboard', - 'container', - 'containerConsole', - 'containerLogs', - 'containerStats', - 'serviceLogs', - 'containers', - 'createContainer', - 'createNetwork', - 'createRegistry', - 'createSecret', - 'createService', - 'createVolume', - 'engine', - 'endpoint', - 'endpointAccess', - 'endpoints', - 'events', - 'image', - 'images', - 'initAdmin', - 'initEndpoint', - 'main', - 'network', - 'networks', - 'node', - 'registries', - 'registry', - 'registryAccess', - 'secrets', - 'secret', - 'service', - 'services', - 'settings', - 'settingsAuthentication', - 'sidebar', - 'swarm', - 'swarmVisualizer', - 'task', - 'team', - 'teams', - 'templates', - 'user', - 'users', - 'userSettings', - 'volume', - 'volumes', - 'rzModule']) - .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) { - 'use strict'; +angular.module('portainer') +.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) { + 'use strict'; - var environment = '@@ENVIRONMENT'; - if (environment === 'production') { - $compileProvider.debugInfoEnabled(false); + EndpointProvider.initialize(); + + StateManager.initialize() + .then(function success(state) { + if (state.application.authentication) { + initAuthentication(authManager, Authentication, $rootScope); } + if (state.application.analytics) { + initAnalytics(Analytics, $rootScope); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + }); - localStorageServiceProvider - .setPrefix('portainer'); + $rootScope.$state = $state; +}]); - jwtOptionsProvider.config({ - tokenGetter: ['LocalStorage', function(LocalStorage) { - return LocalStorage.getJWT(); - }], - unauthenticatedRedirector: ['$state', function($state) { - $state.go('auth', {error: 'Your session has expired'}); - }] - }); - $httpProvider.interceptors.push('jwtInterceptor'); - AnalyticsProvider.setAccount('@@CONFIG_GA_ID'); - AnalyticsProvider.startOffline(true); +function initAuthentication(authManager, Authentication, $rootScope) { + authManager.checkAuthOnRefresh(); + authManager.redirectWhenUnauthenticated(); + Authentication.init(); + $rootScope.$on('tokenHasExpired', function() { + $state.go('auth', {error: 'Your session has expired'}); + }); +} - $urlRouterProvider.otherwise('/auth'); - - toastr.options.timeOut = 3000; - - $uibTooltipProvider.setTriggers({ - 'mouseenter': 'mouseleave', - 'click': 'click', - 'focus': 'blur', - 'outsideClick': 'outsideClick' - }); - - $stateProvider - .state('root', { - abstract: true, - resolve: { - requiresLogin: ['StateManager', function (StateManager) { - var applicationState = StateManager.getState(); - return applicationState.application.authentication; - }] - } - }) - .state('auth', { - parent: 'root', - url: '/auth', - params: { - logout: false, - error: '' - }, - views: { - 'content@': { - templateUrl: 'app/components/auth/auth.html', - controller: 'AuthenticationController' - } - }, - data: { - requiresLogin: false - } - }) - .state('containers', { - parent: 'root', - url: '/containers/', - views: { - 'content@': { - templateUrl: 'app/components/containers/containers.html', - controller: 'ContainersController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('container', { - url: '^/containers/:id', - views: { - 'content@': { - templateUrl: 'app/components/container/container.html', - controller: 'ContainerController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('stats', { - url: '^/containers/:id/stats', - views: { - 'content@': { - templateUrl: 'app/components/containerStats/containerStats.html', - controller: 'ContainerStatsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('containerlogs', { - url: '^/containers/:id/logs', - views: { - 'content@': { - templateUrl: 'app/components/containerLogs/containerlogs.html', - controller: 'ContainerLogsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('servicelogs', { - url: '^/services/:id/logs', - views: { - 'content@': { - templateUrl: 'app/components/serviceLogs/servicelogs.html', - controller: 'ServiceLogsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('console', { - url: '^/containers/:id/console', - views: { - 'content@': { - templateUrl: 'app/components/containerConsole/containerConsole.html', - controller: 'ContainerConsoleController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('dashboard', { - parent: 'root', - url: '/dashboard', - views: { - 'content@': { - templateUrl: 'app/components/dashboard/dashboard.html', - controller: 'DashboardController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('actions', { - abstract: true, - url: '/actions', - views: { - 'content@': { - template: '
' - }, - 'sidebar@': { - template: '
' - } - } - }) - .state('actions.create', { - abstract: true, - url: '/create', - views: { - 'content@': { - template: '
' - }, - 'sidebar@': { - template: '
' - } - } - }) - .state('actions.create.container', { - url: '/container/:from', - views: { - 'content@': { - templateUrl: 'app/components/createContainer/createcontainer.html', - controller: 'CreateContainerController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('actions.create.network', { - url: '/network', - views: { - 'content@': { - templateUrl: 'app/components/createNetwork/createnetwork.html', - controller: 'CreateNetworkController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('actions.create.registry', { - url: '/registry', - views: { - 'content@': { - templateUrl: 'app/components/createRegistry/createregistry.html', - controller: 'CreateRegistryController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('actions.create.secret', { - url: '/secret', - views: { - 'content@': { - templateUrl: 'app/components/createSecret/createsecret.html', - controller: 'CreateSecretController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('actions.create.service', { - url: '/service', - views: { - 'content@': { - templateUrl: 'app/components/createService/createservice.html', - controller: 'CreateServiceController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('actions.create.volume', { - url: '/volume', - views: { - 'content@': { - templateUrl: 'app/components/createVolume/createvolume.html', - controller: 'CreateVolumeController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('init', { - abstract: true, - url: '/init', - views: { - 'content@': { - template: '
' - } - } - }) - .state('init.endpoint', { - url: '/endpoint', - views: { - 'content@': { - templateUrl: 'app/components/initEndpoint/initEndpoint.html', - controller: 'InitEndpointController' - } - } - }) - .state('init.admin', { - url: '/admin', - views: { - 'content@': { - templateUrl: 'app/components/initAdmin/initAdmin.html', - controller: 'InitAdminController' - } - } - }) - .state('engine', { - url: '/engine/', - views: { - 'content@': { - templateUrl: 'app/components/engine/engine.html', - controller: 'EngineController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('endpoints', { - url: '/endpoints/', - views: { - 'content@': { - templateUrl: 'app/components/endpoints/endpoints.html', - controller: 'EndpointsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('endpoint', { - url: '^/endpoints/:id', - views: { - 'content@': { - templateUrl: 'app/components/endpoint/endpoint.html', - controller: 'EndpointController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('endpoint.access', { - url: '^/endpoints/:id/access', - views: { - 'content@': { - templateUrl: 'app/components/endpointAccess/endpointAccess.html', - controller: 'EndpointAccessController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('events', { - url: '/events/', - views: { - 'content@': { - templateUrl: 'app/components/events/events.html', - controller: 'EventsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('images', { - url: '/images/', - views: { - 'content@': { - templateUrl: 'app/components/images/images.html', - controller: 'ImagesController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('image', { - url: '^/images/:id/', - views: { - 'content@': { - templateUrl: 'app/components/image/image.html', - controller: 'ImageController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('networks', { - url: '/networks/', - views: { - 'content@': { - templateUrl: 'app/components/networks/networks.html', - controller: 'NetworksController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('network', { - url: '^/networks/:id/', - views: { - 'content@': { - templateUrl: 'app/components/network/network.html', - controller: 'NetworkController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('node', { - url: '^/nodes/:id/', - views: { - 'content@': { - templateUrl: 'app/components/node/node.html', - controller: 'NodeController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('registries', { - url: '/registries/', - views: { - 'content@': { - templateUrl: 'app/components/registries/registries.html', - controller: 'RegistriesController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('registry', { - url: '^/registries/:id', - views: { - 'content@': { - templateUrl: 'app/components/registry/registry.html', - controller: 'RegistryController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('registry.access', { - url: '^/registries/:id/access', - views: { - 'content@': { - templateUrl: 'app/components/registryAccess/registryAccess.html', - controller: 'RegistryAccessController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('secrets', { - url: '^/secrets/', - views: { - 'content@': { - templateUrl: 'app/components/secrets/secrets.html', - controller: 'SecretsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('secret', { - url: '^/secret/:id/', - views: { - 'content@': { - templateUrl: 'app/components/secret/secret.html', - controller: 'SecretController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('services', { - url: '/services/', - views: { - 'content@': { - templateUrl: 'app/components/services/services.html', - controller: 'ServicesController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('service', { - url: '^/service/:id/', - views: { - 'content@': { - templateUrl: 'app/components/service/service.html', - controller: 'ServiceController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('settings', { - url: '/settings/', - views: { - 'content@': { - templateUrl: 'app/components/settings/settings.html', - controller: 'SettingsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('settings_authentication', { - url: '^/settings/authentication', - views: { - 'content@': { - templateUrl: 'app/components/settingsAuthentication/settingsAuthentication.html', - controller: 'SettingsAuthenticationController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('task', { - url: '^/task/:id', - views: { - 'content@': { - templateUrl: 'app/components/task/task.html', - controller: 'TaskController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('templates', { - url: '/templates/', - params: { - key: 'containers', - hide_descriptions: false - }, - views: { - 'content@': { - templateUrl: 'app/components/templates/templates.html', - controller: 'TemplatesController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('templates_linuxserver', { - url: '^/templates/linuxserver.io', - params: { - key: 'linuxserver.io', - hide_descriptions: true - }, - views: { - 'content@': { - templateUrl: 'app/components/templates/templates.html', - controller: 'TemplatesController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('volumes', { - url: '/volumes/', - views: { - 'content@': { - templateUrl: 'app/components/volumes/volumes.html', - controller: 'VolumesController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('volume', { - url: '^/volumes/:id', - views: { - 'content@': { - templateUrl: 'app/components/volume/volume.html', - controller: 'VolumeController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('users', { - url: '/users/', - views: { - 'content@': { - templateUrl: 'app/components/users/users.html', - controller: 'UsersController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('user', { - url: '^/users/:id', - views: { - 'content@': { - templateUrl: 'app/components/user/user.html', - controller: 'UserController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('userSettings', { - url: '/userSettings/', - views: { - 'content@': { - templateUrl: 'app/components/userSettings/userSettings.html', - controller: 'UserSettingsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('teams', { - url: '/teams/', - views: { - 'content@': { - templateUrl: 'app/components/teams/teams.html', - controller: 'TeamsController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('team', { - url: '^/teams/:id', - views: { - 'content@': { - templateUrl: 'app/components/team/team.html', - controller: 'TeamController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('swarm', { - url: '/swarm', - views: { - 'content@': { - templateUrl: 'app/components/swarm/swarm.html', - controller: 'SwarmController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - .state('swarm.visualizer', { - url: '/visualizer', - views: { - 'content@': { - templateUrl: 'app/components/swarmVisualizer/swarmVisualizer.html', - controller: 'SwarmVisualizerController' - }, - 'sidebar@': { - templateUrl: 'app/components/sidebar/sidebar.html', - controller: 'SidebarController' - } - } - }) - ; - }]) - .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) { - EndpointProvider.initialize(); - StateManager.initialize().then(function success(state) { - if (state.application.authentication) { - authManager.checkAuthOnRefresh(); - authManager.redirectWhenUnauthenticated(); - Authentication.init(); - $rootScope.$on('tokenHasExpired', function($state) { - $state.go('auth', {error: 'Your session has expired'}); - }); - } - if (state.application.analytics) { - Analytics.offline(false); - Analytics.registerScriptTags(); - Analytics.registerTrackers(); - $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) { - Analytics.trackPage(toState.url); - Analytics.pageView(); - }); - } - }, function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve application settings'); - }); - - $rootScope.$state = $state; - }]) - // This is your docker url that the api will use to make requests - // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 - // .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243 - .constant('API_ENDPOINT_AUTH', 'api/auth') - .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') - .constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints') - .constant('API_ENDPOINT_REGISTRIES', 'api/registries') - .constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') - .constant('API_ENDPOINT_SETTINGS', 'api/settings') - .constant('API_ENDPOINT_STATUS', 'api/status') - .constant('API_ENDPOINT_USERS', 'api/users') - .constant('API_ENDPOINT_TEAMS', 'api/teams') - .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships') - .constant('API_ENDPOINT_TEMPLATES', 'api/templates') - .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') - .constant('PAGINATION_MAX_ITEMS', 10); +function initAnalytics(Analytics, $rootScope) { + Analytics.offline(false); + Analytics.registerScriptTags(); + Analytics.registerTrackers(); + $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) { + Analytics.trackPage(toState.url); + Analytics.pageView(); + }); +} diff --git a/app/config.js b/app/config.js new file mode 100644 index 000000000..197bd6e76 --- /dev/null +++ b/app/config.js @@ -0,0 +1,37 @@ +angular.module('portainer') + .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) { + 'use strict'; + + var environment = '@@ENVIRONMENT'; + if (environment === 'production') { + $compileProvider.debugInfoEnabled(false); + } + + localStorageServiceProvider + .setPrefix('portainer'); + + jwtOptionsProvider.config({ + tokenGetter: ['LocalStorage', function(LocalStorage) { + return LocalStorage.getJWT(); + }], + unauthenticatedRedirector: ['$state', function($state) { + $state.go('auth', {error: 'Your session has expired'}); + }] + }); + $httpProvider.interceptors.push('jwtInterceptor'); + + AnalyticsProvider.setAccount('@@CONFIG_GA_ID'); + AnalyticsProvider.startOffline(true); + + toastr.options.timeOut = 3000; + + $uibTooltipProvider.setTriggers({ + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur', + 'outsideClick': 'outsideClick' + }); + + $urlRouterProvider.otherwise('/auth'); + configureRoutes($stateProvider); + }]); diff --git a/app/constants.js b/app/constants.js new file mode 100644 index 000000000..d62fd0a96 --- /dev/null +++ b/app/constants.js @@ -0,0 +1,14 @@ +angular.module('portainer') +.constant('API_ENDPOINT_AUTH', 'api/auth') +.constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') +.constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints') +.constant('API_ENDPOINT_REGISTRIES', 'api/registries') +.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') +.constant('API_ENDPOINT_SETTINGS', 'api/settings') +.constant('API_ENDPOINT_STATUS', 'api/status') +.constant('API_ENDPOINT_USERS', 'api/users') +.constant('API_ENDPOINT_TEAMS', 'api/teams') +.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships') +.constant('API_ENDPOINT_TEMPLATES', 'api/templates') +.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') +.constant('PAGINATION_MAX_ITEMS', 10); diff --git a/app/filters/__module.js b/app/filters/__module.js new file mode 100644 index 000000000..98133a2a0 --- /dev/null +++ b/app/filters/__module.js @@ -0,0 +1 @@ +angular.module('portainer.filters', []); diff --git a/app/helpers/__module.js b/app/helpers/__module.js new file mode 100644 index 000000000..1621ac914 --- /dev/null +++ b/app/helpers/__module.js @@ -0,0 +1 @@ +angular.module('portainer.helpers', []); diff --git a/app/rest/__module.js b/app/rest/__module.js new file mode 100644 index 000000000..2c8129eb6 --- /dev/null +++ b/app/rest/__module.js @@ -0,0 +1 @@ +angular.module('portainer.rest', ['ngResource']); diff --git a/app/routes.js b/app/routes.js new file mode 100644 index 000000000..a0359ea07 --- /dev/null +++ b/app/routes.js @@ -0,0 +1,662 @@ +function configureRoutes($stateProvider) { + $stateProvider + .state('root', { + abstract: true, + resolve: { + requiresLogin: ['StateManager', function (StateManager) { + var applicationState = StateManager.getState(); + return applicationState.application.authentication; + }] + } + }) + .state('auth', { + parent: 'root', + url: '/auth', + params: { + logout: false, + error: '' + }, + views: { + 'content@': { + templateUrl: 'app/components/auth/auth.html', + controller: 'AuthenticationController' + } + }, + data: { + requiresLogin: false + } + }) + .state('containers', { + parent: 'root', + url: '/containers/', + views: { + 'content@': { + templateUrl: 'app/components/containers/containers.html', + controller: 'ContainersController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('container', { + url: '^/containers/:id', + views: { + 'content@': { + templateUrl: 'app/components/container/container.html', + controller: 'ContainerController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('stats', { + url: '^/containers/:id/stats', + views: { + 'content@': { + templateUrl: 'app/components/containerStats/containerStats.html', + controller: 'ContainerStatsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('containerlogs', { + url: '^/containers/:id/logs', + views: { + 'content@': { + templateUrl: 'app/components/containerLogs/containerlogs.html', + controller: 'ContainerLogsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('servicelogs', { + url: '^/services/:id/logs', + views: { + 'content@': { + templateUrl: 'app/components/serviceLogs/servicelogs.html', + controller: 'ServiceLogsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('console', { + url: '^/containers/:id/console', + views: { + 'content@': { + templateUrl: 'app/components/containerConsole/containerConsole.html', + controller: 'ContainerConsoleController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('dashboard', { + parent: 'root', + url: '/dashboard', + views: { + 'content@': { + templateUrl: 'app/components/dashboard/dashboard.html', + controller: 'DashboardController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('actions', { + abstract: true, + url: '/actions', + views: { + 'content@': { + template: '
' + }, + 'sidebar@': { + template: '
' + } + } + }) + .state('actions.create', { + abstract: true, + url: '/create', + views: { + 'content@': { + template: '
' + }, + 'sidebar@': { + template: '
' + } + } + }) + .state('actions.create.container', { + url: '/container/:from', + views: { + 'content@': { + templateUrl: 'app/components/createContainer/createcontainer.html', + controller: 'CreateContainerController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('actions.create.network', { + url: '/network', + views: { + 'content@': { + templateUrl: 'app/components/createNetwork/createnetwork.html', + controller: 'CreateNetworkController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('actions.create.registry', { + url: '/registry', + views: { + 'content@': { + templateUrl: 'app/components/createRegistry/createregistry.html', + controller: 'CreateRegistryController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('actions.create.secret', { + url: '/secret', + views: { + 'content@': { + templateUrl: 'app/components/createSecret/createsecret.html', + controller: 'CreateSecretController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('actions.create.service', { + url: '/service', + views: { + 'content@': { + templateUrl: 'app/components/createService/createservice.html', + controller: 'CreateServiceController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('actions.create.volume', { + url: '/volume', + views: { + 'content@': { + templateUrl: 'app/components/createVolume/createvolume.html', + controller: 'CreateVolumeController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('init', { + abstract: true, + url: '/init', + views: { + 'content@': { + template: '
' + } + } + }) + .state('init.endpoint', { + url: '/endpoint', + views: { + 'content@': { + templateUrl: 'app/components/initEndpoint/initEndpoint.html', + controller: 'InitEndpointController' + } + } + }) + .state('init.admin', { + url: '/admin', + views: { + 'content@': { + templateUrl: 'app/components/initAdmin/initAdmin.html', + controller: 'InitAdminController' + } + } + }) + .state('engine', { + url: '/engine/', + views: { + 'content@': { + templateUrl: 'app/components/engine/engine.html', + controller: 'EngineController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('endpoints', { + url: '/endpoints/', + views: { + 'content@': { + templateUrl: 'app/components/endpoints/endpoints.html', + controller: 'EndpointsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('endpoint', { + url: '^/endpoints/:id', + views: { + 'content@': { + templateUrl: 'app/components/endpoint/endpoint.html', + controller: 'EndpointController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('endpoint.access', { + url: '^/endpoints/:id/access', + views: { + 'content@': { + templateUrl: 'app/components/endpointAccess/endpointAccess.html', + controller: 'EndpointAccessController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('events', { + url: '/events/', + views: { + 'content@': { + templateUrl: 'app/components/events/events.html', + controller: 'EventsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('images', { + url: '/images/', + views: { + 'content@': { + templateUrl: 'app/components/images/images.html', + controller: 'ImagesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('image', { + url: '^/images/:id/', + views: { + 'content@': { + templateUrl: 'app/components/image/image.html', + controller: 'ImageController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('networks', { + url: '/networks/', + views: { + 'content@': { + templateUrl: 'app/components/networks/networks.html', + controller: 'NetworksController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('network', { + url: '^/networks/:id/', + views: { + 'content@': { + templateUrl: 'app/components/network/network.html', + controller: 'NetworkController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('node', { + url: '^/nodes/:id/', + views: { + 'content@': { + templateUrl: 'app/components/node/node.html', + controller: 'NodeController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('registries', { + url: '/registries/', + views: { + 'content@': { + templateUrl: 'app/components/registries/registries.html', + controller: 'RegistriesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('registry', { + url: '^/registries/:id', + views: { + 'content@': { + templateUrl: 'app/components/registry/registry.html', + controller: 'RegistryController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('registry.access', { + url: '^/registries/:id/access', + views: { + 'content@': { + templateUrl: 'app/components/registryAccess/registryAccess.html', + controller: 'RegistryAccessController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('secrets', { + url: '^/secrets/', + views: { + 'content@': { + templateUrl: 'app/components/secrets/secrets.html', + controller: 'SecretsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('secret', { + url: '^/secret/:id/', + views: { + 'content@': { + templateUrl: 'app/components/secret/secret.html', + controller: 'SecretController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('services', { + url: '/services/', + views: { + 'content@': { + templateUrl: 'app/components/services/services.html', + controller: 'ServicesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('service', { + url: '^/service/:id/', + views: { + 'content@': { + templateUrl: 'app/components/service/service.html', + controller: 'ServiceController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('settings', { + url: '/settings/', + views: { + 'content@': { + templateUrl: 'app/components/settings/settings.html', + controller: 'SettingsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('settings_authentication', { + url: '^/settings/authentication', + views: { + 'content@': { + templateUrl: 'app/components/settingsAuthentication/settingsAuthentication.html', + controller: 'SettingsAuthenticationController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('task', { + url: '^/task/:id', + views: { + 'content@': { + templateUrl: 'app/components/task/task.html', + controller: 'TaskController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('templates', { + url: '/templates/', + params: { + key: 'containers', + hide_descriptions: false + }, + views: { + 'content@': { + templateUrl: 'app/components/templates/templates.html', + controller: 'TemplatesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('templates_linuxserver', { + url: '^/templates/linuxserver.io', + params: { + key: 'linuxserver.io', + hide_descriptions: true + }, + views: { + 'content@': { + templateUrl: 'app/components/templates/templates.html', + controller: 'TemplatesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('volumes', { + url: '/volumes/', + views: { + 'content@': { + templateUrl: 'app/components/volumes/volumes.html', + controller: 'VolumesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('volume', { + url: '^/volumes/:id', + views: { + 'content@': { + templateUrl: 'app/components/volume/volume.html', + controller: 'VolumeController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('users', { + url: '/users/', + views: { + 'content@': { + templateUrl: 'app/components/users/users.html', + controller: 'UsersController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('user', { + url: '^/users/:id', + views: { + 'content@': { + templateUrl: 'app/components/user/user.html', + controller: 'UserController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('userSettings', { + url: '/userSettings/', + views: { + 'content@': { + templateUrl: 'app/components/userSettings/userSettings.html', + controller: 'UserSettingsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('teams', { + url: '/teams/', + views: { + 'content@': { + templateUrl: 'app/components/teams/teams.html', + controller: 'TeamsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('team', { + url: '^/teams/:id', + views: { + 'content@': { + templateUrl: 'app/components/team/team.html', + controller: 'TeamController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('swarm', { + url: '/swarm', + views: { + 'content@': { + templateUrl: 'app/components/swarm/swarm.html', + controller: 'SwarmController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('swarm.visualizer', { + url: '/visualizer', + views: { + 'content@': { + templateUrl: 'app/components/swarmVisualizer/swarmVisualizer.html', + controller: 'SwarmVisualizerController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }); +} diff --git a/app/services/__module.js b/app/services/__module.js new file mode 100644 index 000000000..dc0c26e72 --- /dev/null +++ b/app/services/__module.js @@ -0,0 +1 @@ +angular.module('portainer.services', []); diff --git a/gruntfile.js b/gruntfile.js index 95ee9184e..88f621705 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -58,7 +58,7 @@ module.exports = function (grunt) { prod: { options: { variables: { 'environment': 'production' }}} }, src: { - js: ['app/**/*.js', '!app/**/*.spec.js'], + js: ['app/**/__module.js', 'app/**/*.js', '!app/**/*.spec.js'], jsTpl: ['<%= distdir %>/templates/**/*.js'], html: ['index.html'], tpl: ['app/components/**/*.html', 'app/directives/**/*.html'], From ff628bb438825fe692ce74c944ff24860f9f9209 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 21 Sep 2017 16:00:53 +0200 Subject: [PATCH 03/20] refactor(app): upgrade to the latest version of ui-router (#1219) * refactor(app): upgrade to the latest version of ui-router * fix(app): define optional from parameter in action.create.container state * refactor(app): replace $uiRouterGlobals with $transition$ --- app/components/auth/authController.js | 8 ++-- .../container/containerController.js | 48 +++++++++---------- .../containerConsoleController.js | 8 ++-- .../containerLogs/containerLogsController.js | 10 ++-- .../containerStatsController.js | 14 +++--- .../createContainerController.js | 8 ++-- app/components/endpoint/endpointController.js | 6 +-- .../endpointAccessController.js | 8 ++-- app/components/image/imageController.js | 14 +++--- app/components/network/networkController.js | 16 +++---- app/components/node/nodeController.js | 6 +-- app/components/registry/registryController.js | 6 +-- .../registryAccessController.js | 8 ++-- app/components/secret/secretController.js | 6 +-- app/components/secrets/secretsController.js | 4 +- app/components/service/serviceController.js | 6 +-- .../serviceLogs/serviceLogsController.js | 10 ++-- app/components/services/servicesController.js | 4 +- app/components/task/taskController.js | 6 +-- app/components/team/teamController.js | 8 ++-- .../templates/templatesController.js | 8 ++-- app/components/user/userController.js | 6 +-- app/components/volume/volumeController.js | 8 ++-- app/routes.js | 3 ++ bower.json | 4 +- 25 files changed, 118 insertions(+), 115 deletions(-) diff --git a/app/components/auth/authController.js b/app/components/auth/authController.js index a20d0ccb7..81a7fc05d 100644 --- a/app/components/auth/authController.js +++ b/app/components/auth/authController.js @@ -1,6 +1,6 @@ angular.module('auth', []) -.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService', -function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) { +.controller('AuthenticationController', ['$scope', '$state', '$transition$', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService', +function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) { $scope.logo = StateManager.getState().application.logo; @@ -88,9 +88,9 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentica }; function initView() { - if ($stateParams.logout || $stateParams.error) { + if ($transition$.params().logout || $transition$.params().error) { Authentication.logout(); - $scope.state.AuthenticationError = $stateParams.error; + $scope.state.AuthenticationError = $transition$.params().error; return; } diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index 1908aa8d4..2ab871f48 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,6 +1,6 @@ angular.module('container', []) -.controller('ContainerController', ['$q', '$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', -function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) { +.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', +function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) { $scope.activityTime = 0; $scope.portBindings = []; $scope.config = { @@ -16,7 +16,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, var update = function () { $('#loadingViewSpinner').show(); - Container.get({id: $stateParams.id}, function (d) { + Container.get({id: $transition$.params().id}, function (d) { var container = new ContainerDetailsViewModel(d); $scope.container = container; $scope.container.edit = false; @@ -52,7 +52,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $('#loadingViewSpinner').show(); Container.start({id: $scope.container.Id}, {}, function (d) { update(); - Notifications.success('Container started', $stateParams.id); + Notifications.success('Container started', $transition$.params().id); }, function (e) { update(); Notifications.error('Failure', e, 'Unable to start container'); @@ -61,9 +61,9 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.stop = function () { $('#loadingViewSpinner').show(); - Container.stop({id: $stateParams.id}, function (d) { + Container.stop({id: $transition$.params().id}, function (d) { update(); - Notifications.success('Container stopped', $stateParams.id); + Notifications.success('Container stopped', $transition$.params().id); }, function (e) { update(); Notifications.error('Failure', e, 'Unable to stop container'); @@ -72,9 +72,9 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.kill = function () { $('#loadingViewSpinner').show(); - Container.kill({id: $stateParams.id}, function (d) { + Container.kill({id: $transition$.params().id}, function (d) { update(); - Notifications.success('Container killed', $stateParams.id); + Notifications.success('Container killed', $transition$.params().id); }, function (e) { update(); Notifications.error('Failure', e, 'Unable to kill container'); @@ -86,10 +86,10 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, var image = $scope.config.Image; var registry = $scope.config.Registry; var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL); - ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) { + ContainerCommit.commit({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) { $('#createImageSpinner').hide(); update(); - Notifications.success('Container commited', $stateParams.id); + Notifications.success('Container commited', $transition$.params().id); }, function (e) { $('#createImageSpinner').hide(); update(); @@ -99,9 +99,9 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.pause = function () { $('#loadingViewSpinner').show(); - Container.pause({id: $stateParams.id}, function (d) { + Container.pause({id: $transition$.params().id}, function (d) { update(); - Notifications.success('Container paused', $stateParams.id); + Notifications.success('Container paused', $transition$.params().id); }, function (e) { update(); Notifications.error('Failure', e, 'Unable to pause container'); @@ -110,9 +110,9 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.unpause = function () { $('#loadingViewSpinner').show(); - Container.unpause({id: $stateParams.id}, function (d) { + Container.unpause({id: $transition$.params().id}, function (d) { update(); - Notifications.success('Container unpaused', $stateParams.id); + Notifications.success('Container unpaused', $transition$.params().id); }, function (e) { update(); Notifications.error('Failure', e, 'Unable to unpause container'); @@ -154,9 +154,9 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.restart = function () { $('#loadingViewSpinner').show(); - Container.restart({id: $stateParams.id}, function (d) { + Container.restart({id: $transition$.params().id}, function (d) { update(); - Notifications.success('Container restarted', $stateParams.id); + Notifications.success('Container restarted', $transition$.params().id); }, function (e) { update(); Notifications.error('Failure', e, 'Unable to restart container'); @@ -165,7 +165,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.renameContainer = function () { var container = $scope.container; - Container.rename({id: $stateParams.id, 'name': container.newContainerName}, function (d) { + Container.rename({id: $transition$.params().id, 'name': container.newContainerName}, function (d) { if (d.message) { container.newContainerName = container.Name; Notifications.error('Unable to rename container', {}, d.message); @@ -181,14 +181,14 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) { $('#loadingViewSpinner').show(); - Network.disconnect({id: networkId}, { Container: $stateParams.id, Force: false }, function (d) { + Network.disconnect({id: networkId}, { Container: $transition$.params().id, Force: false }, function (d) { if (container.message) { $('#loadingViewSpinner').hide(); Notifications.error('Error', d, 'Unable to disconnect container from network'); } else { $('#loadingViewSpinner').hide(); - Notifications.success('Container left network', $stateParams.id); - $state.go('container', {id: $stateParams.id}, {reload: true}); + Notifications.success('Container left network', $transition$.params().id); + $state.go('container', {id: $transition$.params().id}, {reload: true}); } }, function (e) { $('#loadingViewSpinner').hide(); @@ -199,7 +199,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.duplicate = function() { ModalService.confirmExperimentalFeature(function (experimental) { if(!experimental) { return; } - $state.go('actions.create.container', {from: $stateParams.id}, {reload: true}); + $state.go('actions.create.container', {from: $transition$.params().id}, {reload: true}); }); }; @@ -280,14 +280,14 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, $scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) { $('#joinNetworkSpinner').show(); - Network.connect({id: networkId}, { Container: $stateParams.id }, function (d) { + Network.connect({id: networkId}, { Container: $transition$.params().id }, function (d) { if (container.message) { $('#joinNetworkSpinner').hide(); Notifications.error('Error', d, 'Unable to connect container to network'); } else { $('#joinNetworkSpinner').hide(); - Notifications.success('Container joined network', $stateParams.id); - $state.go('container', {id: $stateParams.id}, {reload: true}); + Notifications.success('Container joined network', $transition$.params().id); + $state.go('container', {id: $transition$.params().id}, {reload: true}); } }, function (e) { $('#joinNetworkSpinner').hide(); diff --git a/app/components/containerConsole/containerConsoleController.js b/app/components/containerConsole/containerConsoleController.js index aa37611f4..949a72f12 100644 --- a/app/components/containerConsole/containerConsoleController.js +++ b/app/components/containerConsole/containerConsoleController.js @@ -1,6 +1,6 @@ angular.module('containerConsole', []) -.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ContainerService', 'ExecService', -function ($scope, $stateParams, Container, Image, EndpointProvider, Notifications, ContainerHelper, ContainerService, ExecService) { +.controller('ContainerConsoleController', ['$scope', '$transition$', 'Container', 'Image', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ContainerService', 'ExecService', +function ($scope, $transition$, Container, Image, EndpointProvider, Notifications, ContainerHelper, ContainerService, ExecService) { $scope.state = {}; $scope.state.loaded = false; $scope.state.connected = false; @@ -15,7 +15,7 @@ function ($scope, $stateParams, Container, Image, EndpointProvider, Notification } }); - Container.get({id: $stateParams.id}, function(d) { + Container.get({id: $transition$.params().id}, function(d) { $scope.container = d; if (d.message) { Notifications.error('Error', d, 'Unable to retrieve container details'); @@ -43,7 +43,7 @@ function ($scope, $stateParams, Container, Image, EndpointProvider, Notification var command = $scope.formValues.isCustomCommand ? $scope.formValues.customCommand : $scope.formValues.command; var execConfig = { - id: $stateParams.id, + id: $transition$.params().id, AttachStdin: true, AttachStdout: true, AttachStderr: true, diff --git a/app/components/containerLogs/containerLogsController.js b/app/components/containerLogs/containerLogsController.js index 86c2eb398..ced15ee57 100644 --- a/app/components/containerLogs/containerLogsController.js +++ b/app/components/containerLogs/containerLogsController.js @@ -1,6 +1,6 @@ angular.module('containerLogs', []) -.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container', -function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) { +.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', +function ($scope, $transition$, $anchorScroll, ContainerLogs, Container) { $scope.state = {}; $scope.state.displayTimestampsOut = false; $scope.state.displayTimestampsErr = false; @@ -9,7 +9,7 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) { $scope.tailLines = 2000; $('#loadingViewSpinner').show(); - Container.get({id: $stateParams.id}, function (d) { + Container.get({id: $transition$.params().id}, function (d) { $scope.container = d; $('#loadingViewSpinner').hide(); }, function (e) { @@ -25,7 +25,7 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) { } function getLogsStderr() { - ContainerLogs.get($stateParams.id, { + ContainerLogs.get($transition$.params().id, { stdout: 0, stderr: 1, timestamps: $scope.state.displayTimestampsErr, @@ -41,7 +41,7 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) { } function getLogsStdout() { - ContainerLogs.get($stateParams.id, { + ContainerLogs.get($transition$.params().id, { stdout: 1, stderr: 0, timestamps: $scope.state.displayTimestampsOut, diff --git a/app/components/containerStats/containerStatsController.js b/app/components/containerStats/containerStatsController.js index a48383507..2992c04f5 100644 --- a/app/components/containerStats/containerStatsController.js +++ b/app/components/containerStats/containerStatsController.js @@ -1,6 +1,6 @@ angular.module('containerStats', []) -.controller('ContainerStatsController', ['$q', '$scope', '$stateParams', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'Pagination', -function ($q, $scope, $stateParams, $document, $interval, ContainerService, ChartService, Notifications, Pagination) { +.controller('ContainerStatsController', ['$q', '$scope', '$transition$', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'Pagination', +function ($q, $scope, $transition$, $document, $interval, ContainerService, ChartService, Notifications, Pagination) { $scope.state = { refreshRate: '5' @@ -79,8 +79,8 @@ function ($q, $scope, $stateParams, $document, $interval, ContainerService, Char function startChartUpdate(networkChart, cpuChart, memoryChart) { $('#loadingViewSpinner').show(); $q.all({ - stats: ContainerService.containerStats($stateParams.id), - top: ContainerService.containerTop($stateParams.id) + stats: ContainerService.containerStats($transition$.params().id), + top: ContainerService.containerTop($transition$.params().id) }) .then(function success(data) { var stats = data.stats; @@ -103,8 +103,8 @@ function ($q, $scope, $stateParams, $document, $interval, ContainerService, Char var refreshRate = $scope.state.refreshRate; $scope.repeater = $interval(function() { $q.all({ - stats: ContainerService.containerStats($stateParams.id), - top: ContainerService.containerTop($stateParams.id) + stats: ContainerService.containerStats($transition$.params().id), + top: ContainerService.containerTop($transition$.params().id) }) .then(function success(data) { var stats = data.stats; @@ -139,7 +139,7 @@ function ($q, $scope, $stateParams, $document, $interval, ContainerService, Char function initView() { $('#loadingViewSpinner').show(); - ContainerService.container($stateParams.id) + ContainerService.container($transition$.params().id) .then(function success(data) { $scope.container = data; }) diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index 5997747f7..a9da83b59 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createContainer', []) -.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', -function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', +function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService) { $scope.formValues = { alwaysPull: true, @@ -418,7 +418,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, function loadFromContainerSpec() { // Get container - Container.get({ id: $stateParams.from }).$promise + Container.get({ id: $transition$.params().from }).$promise .then(function success(d) { var fromContainer = new ContainerDetailsViewModel(d); if (!fromContainer.ResourceControl) { @@ -472,7 +472,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Container.query({}, function (d) { var containers = d; $scope.runningContainers = containers; - if ($stateParams.from !== '') { + if ($transition$.params().from !== '') { loadFromContainerSpec(); } else { $scope.fromContainer = {}; diff --git a/app/components/endpoint/endpointController.js b/app/components/endpoint/endpointController.js index bb3803f93..6b353c9d8 100644 --- a/app/components/endpoint/endpointController.js +++ b/app/components/endpoint/endpointController.js @@ -1,6 +1,6 @@ angular.module('endpoint', []) -.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Notifications', -function ($scope, $state, $stateParams, $filter, EndpointService, Notifications) { +.controller('EndpointController', ['$scope', '$state', '$transition$', '$filter', 'EndpointService', 'Notifications', +function ($scope, $state, $transition$, $filter, EndpointService, Notifications) { if (!$scope.applicationState.application.endpointManagement) { $state.go('endpoints'); @@ -51,7 +51,7 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications) function initView() { $('#loadingViewSpinner').show(); - EndpointService.endpoint($stateParams.id) + EndpointService.endpoint($transition$.params().id) .then(function success(data) { var endpoint = data; endpoint.URL = $filter('stripprotocol')(endpoint.URL); diff --git a/app/components/endpointAccess/endpointAccessController.js b/app/components/endpointAccess/endpointAccessController.js index 413fded43..06206418a 100644 --- a/app/components/endpointAccess/endpointAccessController.js +++ b/app/components/endpointAccess/endpointAccessController.js @@ -1,14 +1,14 @@ angular.module('endpointAccess', []) -.controller('EndpointAccessController', ['$scope', '$stateParams', 'EndpointService', 'Notifications', -function ($scope, $stateParams, EndpointService, Notifications) { +.controller('EndpointAccessController', ['$scope', '$transition$', 'EndpointService', 'Notifications', +function ($scope, $transition$, EndpointService, Notifications) { $scope.updateAccess = function(authorizedUsers, authorizedTeams) { - return EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams); + return EndpointService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams); }; function initView() { $('#loadingViewSpinner').show(); - EndpointService.endpoint($stateParams.id) + EndpointService.endpoint($transition$.params().id) .then(function success(data) { $scope.endpoint = data; }) diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index d76dfb356..394c7956a 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -1,6 +1,6 @@ angular.module('image', []) -.controller('ImageController', ['$q', '$scope', '$stateParams', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', -function ($q, $scope, $stateParams, $state, $timeout, ImageService, RegistryService, Notifications) { +.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', +function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryService, Notifications) { $scope.formValues = { Image: '', Registry: '' @@ -25,10 +25,10 @@ function ($q, $scope, $stateParams, $state, $timeout, ImageService, RegistryServ var image = $scope.formValues.Image; var registry = $scope.formValues.Registry; - ImageService.tagImage($stateParams.id, image, registry.URL) + ImageService.tagImage($transition$.params().id, image, registry.URL) .then(function success(data) { Notifications.success('Image successfully tagged'); - $state.go('image', {id: $stateParams.id}, {reload: true}); + $state.go('image', {id: $transition$.params().id}, {reload: true}); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to tag image'); @@ -83,7 +83,7 @@ function ($q, $scope, $stateParams, $state, $timeout, ImageService, RegistryServ $state.go('images', {}, {reload: true}); } else { Notifications.success('Tag successfully deleted', repository); - $state.go('image', {id: $stateParams.id}, {reload: true}); + $state.go('image', {id: $transition$.params().id}, {reload: true}); } }) .catch(function error(err) { @@ -113,8 +113,8 @@ function ($q, $scope, $stateParams, $state, $timeout, ImageService, RegistryServ $('#loadingViewSpinner').show(); var endpointProvider = $scope.applicationState.endpoint.mode.provider; $q.all({ - image: ImageService.image($stateParams.id), - history: endpointProvider !== 'VMWARE_VIC' ? ImageService.history($stateParams.id) : [] + image: ImageService.image($transition$.params().id), + history: endpointProvider !== 'VMWARE_VIC' ? ImageService.history($transition$.params().id) : [] }) .then(function success(data) { $scope.image = data.image; diff --git a/app/components/network/networkController.js b/app/components/network/networkController.js index 0af567512..f4ea325e5 100644 --- a/app/components/network/networkController.js +++ b/app/components/network/networkController.js @@ -1,16 +1,16 @@ angular.module('network', []) -.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'NetworkService', 'Container', 'ContainerHelper', 'Notifications', -function ($scope, $state, $stateParams, $filter, Network, NetworkService, Container, ContainerHelper, Notifications) { +.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'Network', 'NetworkService', 'Container', 'ContainerHelper', 'Notifications', +function ($scope, $state, $transition$, $filter, Network, NetworkService, Container, ContainerHelper, Notifications) { $scope.removeNetwork = function removeNetwork(networkId) { $('#loadingViewSpinner').show(); - Network.remove({id: $stateParams.id}, function (d) { + Network.remove({id: $transition$.params().id}, function (d) { if (d.message) { $('#loadingViewSpinner').hide(); Notifications.error('Error', d, 'Unable to remove network'); } else { $('#loadingViewSpinner').hide(); - Notifications.success('Network removed', $stateParams.id); + Notifications.success('Network removed', $transition$.params().id); $state.go('networks', {}); } }, function (e) { @@ -21,13 +21,13 @@ function ($scope, $state, $stateParams, $filter, Network, NetworkService, Contai $scope.containerLeaveNetwork = function containerLeaveNetwork(network, containerId) { $('#loadingViewSpinner').show(); - Network.disconnect({id: $stateParams.id}, { Container: containerId, Force: false }, function (d) { + Network.disconnect({id: $transition$.params().id}, { Container: containerId, Force: false }, function (d) { if (d.message) { $('#loadingViewSpinner').hide(); Notifications.error('Error', d, 'Unable to disconnect container from network'); } else { $('#loadingViewSpinner').hide(); - Notifications.success('Container left network', $stateParams.id); + Notifications.success('Container left network', $transition$.params().id); $state.go('network', {id: network.Id}, {reload: true}); } }, function (e) { @@ -68,7 +68,7 @@ function ($scope, $state, $stateParams, $filter, Network, NetworkService, Contai }); } else { Container.query({ - filters: {network: [$stateParams.id]} + filters: {network: [$transition$.params().id]} }, function success(data) { filterContainersInNetwork(network, data); $('#loadingViewSpinner').hide(); @@ -82,7 +82,7 @@ function ($scope, $state, $stateParams, $filter, Network, NetworkService, Contai function initView() { $('#loadingViewSpinner').show(); - NetworkService.network($stateParams.id) + NetworkService.network($transition$.params().id) .then(function success(data) { $scope.network = data; var endpointProvider = $scope.applicationState.endpoint.mode.provider; diff --git a/app/components/node/nodeController.js b/app/components/node/nodeController.js index a417c462d..524038c49 100644 --- a/app/components/node/nodeController.js +++ b/app/components/node/nodeController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('node', []) -.controller('NodeController', ['$scope', '$state', '$stateParams', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Pagination', 'Notifications', -function ($scope, $state, $stateParams, LabelHelper, Node, NodeHelper, Task, Pagination, Notifications) { +.controller('NodeController', ['$scope', '$state', '$transition$', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Pagination', 'Notifications', +function ($scope, $state, $transition$, LabelHelper, Node, NodeHelper, Task, Pagination, Notifications) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('node_tasks'); @@ -80,7 +80,7 @@ function ($scope, $state, $stateParams, LabelHelper, Node, NodeHelper, Task, Pag function loadNodeAndTasks() { $scope.loading = true; if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - Node.get({ id: $stateParams.id}, function(d) { + Node.get({ id: $transition$.params().id}, function(d) { if (d.message) { Notifications.error('Failure', e, 'Unable to inspect the node'); } else { diff --git a/app/components/registry/registryController.js b/app/components/registry/registryController.js index 38dbd1b27..9c5adb82c 100644 --- a/app/components/registry/registryController.js +++ b/app/components/registry/registryController.js @@ -1,6 +1,6 @@ angular.module('registry', []) -.controller('RegistryController', ['$scope', '$state', '$stateParams', '$filter', 'RegistryService', 'Notifications', -function ($scope, $state, $stateParams, $filter, RegistryService, Notifications) { +.controller('RegistryController', ['$scope', '$state', '$transition$', '$filter', 'RegistryService', 'Notifications', +function ($scope, $state, $transition$, $filter, RegistryService, Notifications) { $scope.updateRegistry = function() { $('#updateRegistrySpinner').show(); @@ -20,7 +20,7 @@ function ($scope, $state, $stateParams, $filter, RegistryService, Notifications) function initView() { $('#loadingViewSpinner').show(); - var registryID = $stateParams.id; + var registryID = $transition$.params().id; RegistryService.registry(registryID) .then(function success(data) { $scope.registry = data; diff --git a/app/components/registryAccess/registryAccessController.js b/app/components/registryAccess/registryAccessController.js index 9c6efe719..89ce007ea 100644 --- a/app/components/registryAccess/registryAccessController.js +++ b/app/components/registryAccess/registryAccessController.js @@ -1,14 +1,14 @@ angular.module('registryAccess', []) -.controller('RegistryAccessController', ['$scope', '$stateParams', 'RegistryService', 'Notifications', -function ($scope, $stateParams, RegistryService, Notifications) { +.controller('RegistryAccessController', ['$scope', '$transition$', 'RegistryService', 'Notifications', +function ($scope, $transition$, RegistryService, Notifications) { $scope.updateAccess = function(authorizedUsers, authorizedTeams) { - return RegistryService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams); + return RegistryService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams); }; function initView() { $('#loadingViewSpinner').show(); - RegistryService.registry($stateParams.id) + RegistryService.registry($transition$.params().id) .then(function success(data) { $scope.registry = data; }) diff --git a/app/components/secret/secretController.js b/app/components/secret/secretController.js index 48bfe970e..93944f8c4 100644 --- a/app/components/secret/secretController.js +++ b/app/components/secret/secretController.js @@ -1,6 +1,6 @@ angular.module('secret', []) -.controller('SecretController', ['$scope', '$stateParams', '$state', 'SecretService', 'Notifications', -function ($scope, $stateParams, $state, SecretService, Notifications) { +.controller('SecretController', ['$scope', '$transition$', '$state', 'SecretService', 'Notifications', +function ($scope, $transition$, $state, SecretService, Notifications) { $scope.removeSecret = function removeSecret(secretId) { $('#loadingViewSpinner').show(); @@ -19,7 +19,7 @@ function ($scope, $stateParams, $state, SecretService, Notifications) { function initView() { $('#loadingViewSpinner').show(); - SecretService.secret($stateParams.id) + SecretService.secret($transition$.params().id) .then(function success(data) { $scope.secret = data; }) diff --git a/app/components/secrets/secretsController.js b/app/components/secrets/secretsController.js index 055aa07ee..1aa890504 100644 --- a/app/components/secrets/secretsController.js +++ b/app/components/secrets/secretsController.js @@ -1,6 +1,6 @@ angular.module('secrets', []) -.controller('SecretsController', ['$scope', '$stateParams', '$state', 'SecretService', 'Notifications', 'Pagination', -function ($scope, $stateParams, $state, SecretService, Notifications, Pagination) { +.controller('SecretsController', ['$scope', '$transition$', '$state', 'SecretService', 'Notifications', 'Pagination', +function ($scope, $transition$, $state, SecretService, Notifications, Pagination) { $scope.state = {}; $scope.state.selectedItemCount = 0; $scope.state.pagination_count = Pagination.getPaginationCount('secrets'); diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 775525aa1..cf54ae214 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -1,6 +1,6 @@ angular.module('service', []) -.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'SecretService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', -function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, SecretService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) { +.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'SecretService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', +function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, SecretService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); @@ -307,7 +307,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, function initView() { $('#loadingViewSpinner').show(); var apiVersion = $scope.applicationState.endpoint.apiVersion; - ServiceService.service($stateParams.id) + ServiceService.service($transition$.params().id) .then(function success(data) { var service = data; $scope.isUpdating = $scope.lastVersion >= service.Version; diff --git a/app/components/serviceLogs/serviceLogsController.js b/app/components/serviceLogs/serviceLogsController.js index 9895bbb6e..0c25122ac 100644 --- a/app/components/serviceLogs/serviceLogsController.js +++ b/app/components/serviceLogs/serviceLogsController.js @@ -1,6 +1,6 @@ angular.module('serviceLogs', []) -.controller('ServiceLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ServiceLogs', 'Service', -function ($scope, $stateParams, $anchorScroll, ServiceLogs, Service) { +.controller('ServiceLogsController', ['$scope', '$transition$', '$anchorScroll', 'ServiceLogs', 'Service', +function ($scope, $transition$, $anchorScroll, ServiceLogs, Service) { $scope.state = {}; $scope.state.displayTimestampsOut = false; $scope.state.displayTimestampsErr = false; @@ -16,7 +16,7 @@ function ($scope, $stateParams, $anchorScroll, ServiceLogs, Service) { } function getLogsStderr() { - ServiceLogs.get($stateParams.id, { + ServiceLogs.get($transition$.params().id, { stdout: 0, stderr: 1, timestamps: $scope.state.displayTimestampsErr, @@ -32,7 +32,7 @@ function ($scope, $stateParams, $anchorScroll, ServiceLogs, Service) { } function getLogsStdout() { - ServiceLogs.get($stateParams.id, { + ServiceLogs.get($transition$.params().id, { stdout: 1, stderr: 0, timestamps: $scope.state.displayTimestampsOut, @@ -49,7 +49,7 @@ function ($scope, $stateParams, $anchorScroll, ServiceLogs, Service) { function getService() { $('#loadingViewSpinner').show(); - Service.get({id: $stateParams.id}, function (d) { + Service.get({id: $transition$.params().id}, function (d) { $scope.service = d; $('#loadingViewSpinner').hide(); }, function (e) { diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js index 0a75cd441..339adb3d1 100644 --- a/app/components/services/servicesController.js +++ b/app/components/services/servicesController.js @@ -1,6 +1,6 @@ angular.module('services', []) -.controller('ServicesController', ['$q', '$scope', '$stateParams', '$state', 'Service', 'ServiceService', 'ServiceHelper', 'Notifications', 'Pagination', 'Task', 'Node', 'NodeHelper', 'ModalService', 'ResourceControlService', -function ($q, $scope, $stateParams, $state, Service, ServiceService, ServiceHelper, Notifications, Pagination, Task, Node, NodeHelper, ModalService, ResourceControlService) { +.controller('ServicesController', ['$q', '$scope', '$transition$', '$state', 'Service', 'ServiceService', 'ServiceHelper', 'Notifications', 'Pagination', 'Task', 'Node', 'NodeHelper', 'ModalService', 'ResourceControlService', +function ($q, $scope, $transition$, $state, Service, ServiceService, ServiceHelper, Notifications, Pagination, Task, Node, NodeHelper, ModalService, ResourceControlService) { $scope.state = {}; $scope.state.selectedItemCount = 0; $scope.state.pagination_count = Pagination.getPaginationCount('services'); diff --git a/app/components/task/taskController.js b/app/components/task/taskController.js index 819c8c0d6..39c41feb7 100644 --- a/app/components/task/taskController.js +++ b/app/components/task/taskController.js @@ -1,10 +1,10 @@ angular.module('task', []) -.controller('TaskController', ['$scope', '$stateParams', 'TaskService', 'Service', 'Notifications', -function ($scope, $stateParams, TaskService, Service, Notifications) { +.controller('TaskController', ['$scope', '$transition$', 'TaskService', 'Service', 'Notifications', +function ($scope, $transition$, TaskService, Service, Notifications) { function initView() { $('#loadingViewSpinner').show(); - TaskService.task($stateParams.id) + TaskService.task($transition$.params().id) .then(function success(data) { var task = data; $scope.task = task; diff --git a/app/components/team/teamController.js b/app/components/team/teamController.js index 694432253..ae57c2813 100644 --- a/app/components/team/teamController.js +++ b/app/components/team/teamController.js @@ -1,6 +1,6 @@ angular.module('team', []) -.controller('TeamController', ['$q', '$scope', '$state', '$stateParams', 'TeamService', 'UserService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', -function ($q, $scope, $state, $stateParams, TeamService, UserService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) { +.controller('TeamController', ['$q', '$scope', '$state', '$transition$', 'TeamService', 'UserService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', +function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) { $scope.state = { pagination_count_users: Pagination.getPaginationCount('team_available_users'), @@ -208,9 +208,9 @@ function ($q, $scope, $state, $stateParams, TeamService, UserService, TeamMember $('#loadingViewSpinner').show(); $scope.isAdmin = Authentication.getUserDetails().role === 1 ? true: false; $q.all({ - team: TeamService.team($stateParams.id), + team: TeamService.team($transition$.params().id), users: UserService.users(false), - memberships: TeamService.userMemberships($stateParams.id) + memberships: TeamService.userMemberships($transition$.params().id) }) .then(function success(data) { var users = data.users; diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 347b11d9e..52a3c9de0 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -1,10 +1,10 @@ angular.module('templates', []) -.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', -function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator) { +.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', +function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator) { $scope.state = { selectedTemplate: null, showAdvancedOptions: false, - hideDescriptions: $stateParams.hide_descriptions, + hideDescriptions: $transition$.params().hide_descriptions, formValidationError: '', filters: { Categories: '!', @@ -145,7 +145,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer } function initTemplates() { - var templatesKey = $stateParams.key; + var templatesKey = $transition$.params().key; var provider = $scope.applicationState.endpoint.mode.provider; var apiVersion = $scope.applicationState.endpoint.apiVersion; diff --git a/app/components/user/userController.js b/app/components/user/userController.js index dfb3c0489..fcb6c76b3 100644 --- a/app/components/user/userController.js +++ b/app/components/user/userController.js @@ -1,6 +1,6 @@ angular.module('user', []) -.controller('UserController', ['$q', '$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Notifications', 'SettingsService', -function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications, SettingsService) { +.controller('UserController', ['$q', '$scope', '$state', '$transition$', 'UserService', 'ModalService', 'Notifications', 'SettingsService', +function ($q, $scope, $state, $transition$, UserService, ModalService, Notifications, SettingsService) { $scope.state = { updatePasswordError: '' @@ -72,7 +72,7 @@ function ($q, $scope, $state, $stateParams, UserService, ModalService, Notificat function initView() { $('#loadingViewSpinner').show(); $q.all({ - user: UserService.user($stateParams.id), + user: UserService.user($transition$.params().id), settings: SettingsService.publicSettings() }) .then(function success(data) { diff --git a/app/components/volume/volumeController.js b/app/components/volume/volumeController.js index 2753d1b19..16c165980 100644 --- a/app/components/volume/volumeController.js +++ b/app/components/volume/volumeController.js @@ -1,12 +1,12 @@ angular.module('volume', []) -.controller('VolumeController', ['$scope', '$state', '$stateParams', 'VolumeService', 'Notifications', -function ($scope, $state, $stateParams, VolumeService, Notifications) { +.controller('VolumeController', ['$scope', '$state', '$transition$', 'VolumeService', 'Notifications', +function ($scope, $state, $transition$, VolumeService, Notifications) { $scope.removeVolume = function removeVolume() { $('#loadingViewSpinner').show(); VolumeService.remove($scope.volume) .then(function success(data) { - Notifications.success('Volume successfully removed', $stateParams.id); + Notifications.success('Volume successfully removed', $transition$.params().id); $state.go('volumes', {}); }) .catch(function error(err) { @@ -19,7 +19,7 @@ function ($scope, $state, $stateParams, VolumeService, Notifications) { function initView() { $('#loadingViewSpinner').show(); - VolumeService.volume($stateParams.id) + VolumeService.volume($transition$.params().id) .then(function success(data) { var volume = data; $scope.volume = volume; diff --git a/app/routes.js b/app/routes.js index a0359ea07..15db71679 100644 --- a/app/routes.js +++ b/app/routes.js @@ -154,6 +154,9 @@ function configureRoutes($stateProvider) { templateUrl: 'app/components/sidebar/sidebar.html', controller: 'SidebarController' } + }, + params: { + from: '' } }) .state('actions.create.network', { diff --git a/bower.json b/bower.json index 764a9fb4d..afb3365e0 100644 --- a/bower.json +++ b/bower.json @@ -27,7 +27,6 @@ "angular": "~1.5.0", "angular-cookies": "~1.5.0", "angular-bootstrap": "~2.5.0", - "angular-ui-router": "^0.2.15", "angular-sanitize": "~1.5.0", "angular-mocks": "~1.5.0", "angular-resource": "~1.5.0", @@ -50,7 +49,8 @@ "toastr": "~2.1.3", "xterm.js": "~2.8.1", "chart.js": "~2.6.0", - "angularjs-slider": "^6.4.0" + "angularjs-slider": "^6.4.0", + "angular-ui-router": "~1.0.6" }, "resolutions": { "angular": "1.5.11" From f5749f82d822a44075ae2be7a318e794775fd5ca Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 22 Sep 2017 07:34:17 +0200 Subject: [PATCH 04/20] fix(endpoint-details): fix an issue when updating the local endpoint (#1226) --- app/components/endpoint/endpointController.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/components/endpoint/endpointController.js b/app/components/endpoint/endpointController.js index 6b353c9d8..0d7eef87a 100644 --- a/app/components/endpoint/endpointController.js +++ b/app/components/endpoint/endpointController.js @@ -54,14 +54,13 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications) EndpointService.endpoint($transition$.params().id) .then(function success(data) { var endpoint = data; - endpoint.URL = $filter('stripprotocol')(endpoint.URL); - $scope.endpoint = endpoint; - if (endpoint.URL.indexOf('unix://') === 0) { $scope.endpointType = 'local'; } else { $scope.endpointType = 'remote'; } + endpoint.URL = $filter('stripprotocol')(endpoint.URL); + $scope.endpoint = endpoint; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); From 6bf7c90634c1aedf7dc0df2829f5446e3a107e5b Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 22 Sep 2017 07:45:43 +0200 Subject: [PATCH 05/20] refactor(vendor): relocate angular libraries --- vendor.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vendor.yml b/vendor.yml index 949b6f1ce..d71e768bf 100644 --- a/vendor.yml +++ b/vendor.yml @@ -3,7 +3,6 @@ js: regular: - bower_components/jquery/dist/jquery.js - bower_components/bootstrap/dist/js/bootstrap.js - - bower_components/angular-multi-select/isteven-multi-select.js - bower_components/bootbox.js/bootbox.js - bower_components/filesize/lib/filesize.js - bower_components/lodash/dist/lodash.js @@ -13,11 +12,9 @@ js: - bower_components/toastr/toastr.js - bower_components/xterm.js/dist/xterm.js - bower_components/xterm.js/dist/addons/fit/fit.js - - bower_components/angularjs-slider/dist/rzslider.js minified: - bower_components/jquery/dist/jquery.min.js - bower_components/bootstrap/dist/js/bootstrap.min.js - - bower_components/angular-multi-select/isteven-multi-select.js - bower_components/bootbox.js/bootbox.js - bower_components/Chart.js/Chart.min.js - bower_components/filesize/lib/filesize.min.js @@ -28,7 +25,6 @@ js: - bower_components/toastr/toastr.min.js - bower_components/xterm.js/dist/xterm.js - bower_components/xterm.js/dist/addons/fit/fit.js - - bower_components/angularjs-slider/dist/rzslider.min.js css: regular: - bower_components/bootstrap/dist/css/bootstrap.css @@ -62,6 +58,8 @@ angular: - bower_components/angular-ui-router/release/angular-ui-router.js - bower_components/angular-utils-pagination/dirPagination.js - bower_components/ng-file-upload/ng-file-upload.js + - bower_components/angularjs-slider/dist/rzslider.js + - bower_components/angular-multi-select/isteven-multi-select.js minified: - bower_components/angular/angular.min.js - bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js @@ -75,3 +73,5 @@ angular: - bower_components/angular-ui-router/release/angular-ui-router.min.js - bower_components/angular-utils-pagination/dirPagination.js - bower_components/ng-file-upload/ng-file-upload.min.js + - bower_components/angularjs-slider/dist/rzslider.min.js + - bower_components/angular-multi-select/isteven-multi-select.js From 7b924bde83291ef91ef524b1126087f02e4f270c Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 22 Sep 2017 08:00:13 +0200 Subject: [PATCH 06/20] fix(userSettings): allow to change admin password when using LDAP auth (#1227) --- app/components/userSettings/userSettings.html | 4 ++-- app/components/userSettings/userSettingsController.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/userSettings/userSettings.html b/app/components/userSettings/userSettings.html index c7f5405f7..e9dc4b386 100644 --- a/app/components/userSettings/userSettings.html +++ b/app/components/userSettings/userSettings.html @@ -59,8 +59,8 @@
- - + + You cannot change your password when using LDAP authentication. diff --git a/app/components/userSettings/userSettingsController.js b/app/components/userSettings/userSettingsController.js index 2146d58e0..f746bce81 100644 --- a/app/components/userSettings/userSettingsController.js +++ b/app/components/userSettings/userSettingsController.js @@ -9,11 +9,10 @@ function ($scope, $state, $sanitize, Authentication, UserService, Notifications, $scope.updatePassword = function() { $scope.invalidPassword = false; - var userID = Authentication.getUserDetails().ID; var currentPassword = $sanitize($scope.formValues.currentPassword); var newPassword = $sanitize($scope.formValues.newPassword); - UserService.updateUserPassword(userID, currentPassword, newPassword) + UserService.updateUserPassword($scope.userID, currentPassword, newPassword) .then(function success() { Notifications.success('Success', 'Password successfully updated'); $state.reload(); @@ -28,6 +27,7 @@ function ($scope, $state, $sanitize, Authentication, UserService, Notifications, }; function initView() { + $scope.userID = Authentication.getUserDetails().ID; SettingsService.publicSettings() .then(function success(data) { $scope.AuthenticationMethod = data.AuthenticationMethod; From e2979a631a0879838715cb5d73bfc9162acd43bd Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 22 Sep 2017 08:53:08 +0200 Subject: [PATCH 07/20] style(swarm-visualizer): update font-size (#1228) --- assets/css/app.css | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/css/app.css b/assets/css/app.css index 5a4d2d88a..3f6acad11 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -517,6 +517,7 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5); padding: 10px; margin: 5px; + font-size: 10px; } .visualizer_container .node .tasks .task div { From 6cfffb38f99b1bfec97fd10cd679cdce116ec2e2 Mon Sep 17 00:00:00 2001 From: Nenad Ilic Date: Mon, 25 Sep 2017 18:13:56 +0200 Subject: [PATCH 08/20] feat(cli): Allow adding admin password using docker secrets aka file (#1199) (#1214) --- api/cli/cli.go | 20 +++++++++++++------- api/cmd/portainer/main.go | 21 ++++++++++++++++++--- api/file/file.go | 12 ++++++++++++ api/portainer.go | 1 + 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/api/cli/cli.go b/api/cli/cli.go index dfca35923..c4cde81ec 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -16,12 +16,13 @@ import ( type Service struct{} const ( - errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://") - errSocketNotFound = portainer.Error("Unable to locate Unix socket") - errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") - errInvalidSyncInterval = portainer.Error("Invalid synchronization interval") - errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints") - errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password") + errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://") + errSocketNotFound = portainer.Error("Unable to locate Unix socket") + errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") + errInvalidSyncInterval = portainer.Error("Invalid synchronization interval") + errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints") + errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file") + errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file") ) // ParseFlags parse the CLI flags and return a portainer.Flags struct @@ -45,6 +46,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(), SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(), AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), + AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), // Deprecated flags Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')), Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(), @@ -77,10 +79,14 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { return err } - if *flags.NoAuth && (*flags.AdminPassword != "") { + if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") { return errNoAuthExcludeAdminPassword } + if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" { + return errAdminPassExcludeAdminPassFile + } + displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels) return nil diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 6f202ec32..0b536c895 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -212,12 +212,27 @@ func main() { } } - if *flags.AdminPassword != "" { - log.Printf("Creating admin user with password hash %s", *flags.AdminPassword) + adminPasswordHash := "" + if *flags.AdminPasswordFile != "" { + content, err := file.GetStringFromFile(*flags.AdminPasswordFile) + if err != nil { + log.Fatal(err) + } + adminPasswordHash, err = cryptoService.Hash(content) + if err != nil { + log.Fatal(err) + } + } else if *flags.AdminPassword != "" { + adminPasswordHash = *flags.AdminPassword + } + + if adminPasswordHash != "" { + + log.Printf("Creating admin user with password hash %s", adminPasswordHash) user := &portainer.User{ Username: "admin", Role: portainer.AdministratorRole, - Password: *flags.AdminPassword, + Password: adminPasswordHash, } err := store.UserService.CreateUser(user) if err != nil { diff --git a/api/file/file.go b/api/file/file.go index c143fce0b..80e8d7b12 100644 --- a/api/file/file.go +++ b/api/file/file.go @@ -1,6 +1,8 @@ package file import ( + "io/ioutil" + "github.com/portainer/portainer" "io" @@ -162,3 +164,13 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error { } return nil } + +// GetStringFromFile returns a string content from file. +func GetStringFromFile(filePath string) (string, error) { + content, err := ioutil.ReadFile(filePath) + if err != nil { + return "", err + } + + return string(content), nil +} diff --git a/api/portainer.go b/api/portainer.go index b2a2156a3..539f9745b 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -27,6 +27,7 @@ type ( SSLCert *string SSLKey *string AdminPassword *string + AdminPasswordFile *string // Deprecated fields Logo *string Templates *string From ca9d9b9a77d6186bab47d7b0dce94a005f455c06 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 26 Sep 2017 05:36:51 +0200 Subject: [PATCH 09/20] feat(settings): add a setting to disable bind mounts for non-admins (#1237) * feat(settings): add a setting to disable bind mounts for non-admins * refactor(gruntfile): remove temporary setting --- api/bolt/migrate_dbversion4.go | 16 ++++++++ api/bolt/migrator.go | 8 ++++ api/cmd/portainer/main.go | 1 + api/http/handler/settings.go | 38 ++++++++++--------- api/portainer.go | 15 ++++---- .../createContainerController.js | 14 ++++++- .../createContainer/createcontainer.html | 2 +- .../createService/createServiceController.js | 11 ++++-- .../createService/createservice.html | 2 +- app/components/settings/settings.html | 16 ++++++++ app/components/settings/settingsController.js | 3 ++ app/components/templates/templates.html | 2 +- .../templates/templatesController.js | 11 ++++-- app/models/api/settings/settings.js | 1 + 14 files changed, 105 insertions(+), 35 deletions(-) create mode 100644 api/bolt/migrate_dbversion4.go diff --git a/api/bolt/migrate_dbversion4.go b/api/bolt/migrate_dbversion4.go new file mode 100644 index 000000000..ace64f51e --- /dev/null +++ b/api/bolt/migrate_dbversion4.go @@ -0,0 +1,16 @@ +package bolt + +func (m *Migrator) updateSettingsToVersion5() error { + legacySettings, err := m.SettingsService.Settings() + if err != nil { + return err + } + legacySettings.AllowBindMountsForRegularUsers = true + + err = m.SettingsService.StoreSettings(legacySettings) + if err != nil { + return err + } + + return nil +} diff --git a/api/bolt/migrator.go b/api/bolt/migrator.go index faba56157..2730fe7e9 100644 --- a/api/bolt/migrator.go +++ b/api/bolt/migrator.go @@ -65,6 +65,14 @@ func (m *Migrator) Migrate() error { } } + // https://github.com/portainer/portainer/issues/1235 + if m.CurrentDBVersion < 5 { + err := m.updateSettingsToVersion5() + if err != nil { + return err + } + } + err := m.VersionService.StoreDBVersion(portainer.DBVersion) if err != nil { return err diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 0b536c895..ac8c7f57d 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -125,6 +125,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL portainer.LDAPSearchSettings{}, }, }, + AllowBindMountsForRegularUsers: true, } if *flags.Templates != "" { diff --git a/api/http/handler/settings.go b/api/http/handler/settings.go index 52e957f6d..8e3679e6f 100644 --- a/api/http/handler/settings.go +++ b/api/http/handler/settings.go @@ -45,18 +45,20 @@ func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler { type ( publicSettingsResponse struct { - LogoURL string `json:"LogoURL"` - DisplayExternalContributors bool `json:"DisplayExternalContributors"` - AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + LogoURL string `json:"LogoURL"` + DisplayExternalContributors bool `json:"DisplayExternalContributors"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` } putSettingsRequest struct { - TemplatesURL string `valid:"required"` - LogoURL string `valid:""` - BlackListedLabels []portainer.Pair `valid:""` - DisplayExternalContributors bool `valid:""` - AuthenticationMethod int `valid:"required"` - LDAPSettings portainer.LDAPSettings `valid:""` + TemplatesURL string `valid:"required"` + LogoURL string `valid:""` + BlackListedLabels []portainer.Pair `valid:""` + DisplayExternalContributors bool `valid:""` + AuthenticationMethod int `valid:"required"` + LDAPSettings portainer.LDAPSettings `valid:""` + AllowBindMountsForRegularUsers bool `valid:""` } putSettingsLDAPCheckRequest struct { @@ -85,9 +87,10 @@ func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r } publicSettings := &publicSettingsResponse{ - LogoURL: settings.LogoURL, - DisplayExternalContributors: settings.DisplayExternalContributors, - AuthenticationMethod: settings.AuthenticationMethod, + LogoURL: settings.LogoURL, + DisplayExternalContributors: settings.DisplayExternalContributors, + AuthenticationMethod: settings.AuthenticationMethod, + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, } encodeJSON(w, publicSettings, handler.Logger) @@ -109,11 +112,12 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http } settings := &portainer.Settings{ - TemplatesURL: req.TemplatesURL, - LogoURL: req.LogoURL, - BlackListedLabels: req.BlackListedLabels, - DisplayExternalContributors: req.DisplayExternalContributors, - LDAPSettings: req.LDAPSettings, + TemplatesURL: req.TemplatesURL, + LogoURL: req.LogoURL, + BlackListedLabels: req.BlackListedLabels, + DisplayExternalContributors: req.DisplayExternalContributors, + LDAPSettings: req.LDAPSettings, + AllowBindMountsForRegularUsers: req.AllowBindMountsForRegularUsers, } if req.AuthenticationMethod == 1 { diff --git a/api/portainer.go b/api/portainer.go index 539f9745b..bfbac90a9 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -70,12 +70,13 @@ type ( // Settings represents the application settings. Settings struct { - TemplatesURL string `json:"TemplatesURL"` - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - DisplayExternalContributors bool `json:"DisplayExternalContributors"` - AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` - LDAPSettings LDAPSettings `json:"LDAPSettings"` + TemplatesURL string `json:"TemplatesURL"` + LogoURL string `json:"LogoURL"` + BlackListedLabels []Pair `json:"BlackListedLabels"` + DisplayExternalContributors bool `json:"DisplayExternalContributors"` + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` + LDAPSettings LDAPSettings `json:"LDAPSettings"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` } // User represents a user account. @@ -348,7 +349,7 @@ const ( // APIVersion is the version number of the Portainer API. APIVersion = "1.14.2" // DBVersion is the version number of the Portainer database. - DBVersion = 4 + DBVersion = 5 // DefaultTemplatesURL represents the default URL for the templates definitions. DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" ) diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index a9da83b59..77c44cff7 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createContainer', []) -.controller('CreateContainerController', ['$q', '$scope', '$state', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', -function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SettingsService', +function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SettingsService) { $scope.formValues = { alwaysPull: true, @@ -482,6 +482,16 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, Notifications.error('Failure', e, 'Unable to retrieve running containers'); }); + SettingsService.publicSettings() + .then(function success(data) { + $scope.allowBindMounts = data.AllowBindMountsForRegularUsers; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + }); + + var userDetails = Authentication.getUserDetails(); + $scope.isAdmin = userDetails.role === 1 ? true : false; } function validateForm(accessControlData, isAdmin) { diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index fd282d5b8..fe61cd720 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -235,7 +235,7 @@
-
+
diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index c521d0aae..5b0cb0a62 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createService', []) -.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService', -function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService) { +.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', +function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService, SettingsService) { $scope.formValues = { Name: '', @@ -361,7 +361,8 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, Se volumes: VolumeService.volumes(), secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], networks: NetworkService.networks(true, true, false, false), - nodes: NodeService.nodes() + nodes: NodeService.nodes(), + settings: SettingsService.publicSettings() }) .then(function success(data) { $scope.availableVolumes = data.volumes; @@ -379,6 +380,10 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, Se } else { $scope.state.sliderMaxCpu = 32; } + var settings = data.settings; + $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers; + var userDetails = Authentication.getUserDetails(); + $scope.isAdmin = userDetails.role === 1 ? true : false; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to initialize view'); diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index ad3c254ed..c026df3de 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -223,7 +223,7 @@
-
+
diff --git a/app/components/settings/settings.html b/app/components/settings/settings.html index f5215edb1..d73f4eed6 100644 --- a/app/components/settings/settings.html +++ b/app/components/settings/settings.html @@ -11,6 +11,22 @@
+ +
+ Security +
+
+
+ + +
+
+
Logo diff --git a/app/components/settings/settingsController.js b/app/components/settings/settingsController.js index f74f7def7..c75f97084 100644 --- a/app/components/settings/settingsController.js +++ b/app/components/settings/settingsController.js @@ -6,6 +6,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_ customLogo: false, customTemplates: false, externalContributions: false, + restrictBindMounts: false, labelName: '', labelValue: '' }; @@ -39,6 +40,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_ settings.TemplatesURL = DEFAULT_TEMPLATES_URL; } settings.DisplayExternalContributors = !$scope.formValues.externalContributions; + settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts; updateSettings(settings, false); }; @@ -81,6 +83,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_ $scope.formValues.customTemplates = true; } $scope.formValues.externalContributions = !settings.DisplayExternalContributors; + $scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index bf05cbbfb..8b7172b17 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -154,7 +154,7 @@
- +
-
+
+ @@ -55,7 +63,7 @@
-
+
@@ -65,7 +73,7 @@
-
+
diff --git a/app/components/containerStats/containerStatsController.js b/app/components/containerStats/containerStatsController.js index 2992c04f5..06a8a8618 100644 --- a/app/components/containerStats/containerStatsController.js +++ b/app/components/containerStats/containerStatsController.js @@ -3,7 +3,8 @@ angular.module('containerStats', []) function ($q, $scope, $transition$, $document, $interval, ContainerService, ChartService, Notifications, Pagination) { $scope.state = { - refreshRate: '5' + refreshRate: '5', + networkStatsUnavailable: false }; $scope.state.pagination_count = Pagination.getPaginationCount('stats_processes'); @@ -32,11 +33,13 @@ function ($q, $scope, $transition$, $document, $interval, ContainerService, Char } function updateNetworkChart(stats, chart) { - var rx = stats.Networks[0].rx_bytes; - var tx = stats.Networks[0].tx_bytes; - var label = moment(stats.Date).format('HH:mm:ss'); + if (stats.Networks.length > 0) { + var rx = stats.Networks[0].rx_bytes; + var tx = stats.Networks[0].tx_bytes; + var label = moment(stats.Date).format('HH:mm:ss'); - ChartService.UpdateNetworkChart(label, rx, tx, chart); + ChartService.UpdateNetworkChart(label, rx, tx, chart); + } } function updateMemoryChart(stats, chart) { @@ -85,6 +88,9 @@ function ($q, $scope, $transition$, $document, $interval, ContainerService, Char .then(function success(data) { var stats = data.stats; $scope.processInfo = data.top; + if (stats.Networks.length === 0) { + $scope.state.networkStatsUnavailable = true; + } updateNetworkChart(stats, networkChart); updateMemoryChart(stats, memoryChart); updateCPUChart(stats, cpuChart); From 249bcf5bac27b5cb29bae04603ba2ddc5fbe499d Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 29 Sep 2017 18:44:30 +0200 Subject: [PATCH 12/20] fix(api): prevent the creation of multiple admin users (#1251) --- api/cmd/portainer/main.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index b7091cf7a..74aafde28 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -186,7 +186,6 @@ func main() { applicationStatus := initStatus(authorizeEndpointMgmt, flags) if *flags.Endpoint != "" { - var endpoints []portainer.Endpoint endpoints, err := store.EndpointService.Endpoints() if err != nil { log.Fatal(err) @@ -229,17 +228,25 @@ func main() { } if adminPasswordHash != "" { - - log.Printf("Creating admin user with password hash %s", adminPasswordHash) - user := &portainer.User{ - Username: "admin", - Role: portainer.AdministratorRole, - Password: adminPasswordHash, - } - err := store.UserService.CreateUser(user) + users, err := store.UserService.UsersByRole(portainer.AdministratorRole) if err != nil { log.Fatal(err) } + + if len(users) == 0 { + log.Printf("Creating admin user with password hash %s", adminPasswordHash) + user := &portainer.User{ + Username: "admin", + Role: portainer.AdministratorRole, + Password: adminPasswordHash, + } + err := store.UserService.CreateUser(user) + if err != nil { + log.Fatal(err) + } + } else { + log.Println("Instance already has an administrator user defined. Skipping admin password related flags.") + } } var server portainer.Server = &http.Server{ From 3e99fae07056c13bed885b64f1f83ac1ef1d0379 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 1 Oct 2017 09:44:02 +0100 Subject: [PATCH 13/20] style(sidebar): add a small logo in the sidebar (#1255) --- app/components/sidebar/sidebar.html | 8 +++----- assets/css/app.css | 14 ++++++++++++++ assets/images/logo_small.png | Bin 0 -> 1314 bytes 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 assets/images/logo_small.png diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index aa61d2ce5..b85bb2c64 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -75,11 +75,9 @@ diff --git a/assets/css/app.css b/assets/css/app.css index 3f6acad11..9fda8ca7a 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -296,6 +296,20 @@ ul.sidebar .sidebar-list a.active { background: #2d3e63; } +.sidebar-footer .logo { + width: 100%; + max-width: 100px; + height: 100%; + max-height: 35px; + margin: 2px 0 2px 20px; +} + +.sidebar-footer .version { + font-size: 11px; + margin: 11px 20px 0 7px; + color: #fff; +} + #image-layers .btn{ padding: 0; } diff --git a/assets/images/logo_small.png b/assets/images/logo_small.png new file mode 100644 index 0000000000000000000000000000000000000000..2cd37301aede58a27b98d474e33b83af75e59ae4 GIT binary patch literal 1314 zcmV+-1>O3IP)v_nU>QW2yg7&Rz@sfvWe*tokX>47i%^UHe3+Tfnd!77`obvZVV3XSbH4M>z32Zw z_rfVF5i{qr)S;Yf|I*1e*2~yzt|<96vBr1RE*njPB>*Q@v@%{`BTHGtjT~pEQv;#& zww5=#gU&EKtmZgJ!ZXxyhwHrfWCv0XU|;|N7|lzprGYkUkR*NWPZ_szHm}*jWLjuD zS=S~2nB+YYfV5G@MKm#jpBTX*F60YhNC$yFe9Qge&(bOMCEz3ALdUzNlHoI6 zz0a3=7^!fV3;to$MRr(z0wDIc)_$IDf{{GO@A+J2H)bo;{Q`=vqPlp$5W2XT`T719 ziC6SJ3up+(i+lXNBCFWP3ih#`AK67~ejx>{Gk_O*M)*4^430OJ{vEJRz+oonOVVLh zn(YyWgO%0=gq>7?anB4m$Z=K%%)|pPw=$hzz@e6dBctun#qQ|MDeED0S2$O?+vOgx zIeL;;BkClN`HM&0?r;#}zM$?=pE6;3&I0u@@3zCE?(=H!G7sBon+Lo{!7?}dAb5#? z_=<j@)4gDsnqpp*%7{{fgdZ#f_aJCCSRXl946 zSxp7~SST6k*vRJ%`B;L*zn%T>8=uHlY+W)$&?s}RvImdztUJ5211>)(Zz_>#rDIH=tS=yLr zRpd9+c;@*q0INMS)8yiCuQO4w0;U;3KXtxoYqrT&4>~Ca$RK)AV!J7^y{NTC{CigR z-1xoC$%c8{`z^FiQxTBb(&%{~u((ajjL#}k{wE=fLH1;Kme!g5`W|OYNH>k8a+o@& z#}#QeR#LZ$zS;wjrV_g#wakm1?)Gj#YN5oSq^}|T!2ND Date: Mon, 2 Oct 2017 18:21:42 +0200 Subject: [PATCH 14/20] docs(swagger): update swagger docs --- api/swagger.yaml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/api/swagger.yaml b/api/swagger.yaml index 6499dab61..6a69707c5 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1889,7 +1889,14 @@ definitions: type: "integer" example: 1 description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." - + AllowBindMountsForRegularUsers: + type: "boolean" + example: false + description: "Whether non-administrator should be able to use bind mounts when creating containers" + AllowPrivilegedModeForRegularUsers: + type: "boolean" + example: true + description: "Whether non-administrator should be able to use privileged mode when creating containers" TLSConfiguration: type: "object" properties: @@ -1987,6 +1994,14 @@ definitions: description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." LDAPSettings: $ref: "#/definitions/LDAPSettings" + AllowBindMountsForRegularUsers: + type: "boolean" + example: false + description: "Whether non-administrator should be able to use bind mounts when creating containers" + AllowPrivilegedModeForRegularUsers: + type: "boolean" + example: true + description: "Whether non-administrator should be able to use privileged mode when creating containers" Settings_BlackListedLabels: properties: name: @@ -2394,6 +2409,14 @@ definitions: description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." LDAPSettings: $ref: "#/definitions/LDAPSettings" + AllowBindMountsForRegularUsers: + type: "boolean" + example: true + description: "Whether non-administrator users should be able to use bind mounts when creating containers" + AllowPrivilegedModeForRegularUsers: + type: "boolean" + example: true + description: "Whether non-administrator users should be able to use privileged mode when creating containers" UserCreateRequest: type: "object" required: From f678d05088c72a6ebf2906b09073f5987928268a Mon Sep 17 00:00:00 2001 From: pc Date: Tue, 3 Oct 2017 17:38:30 +0800 Subject: [PATCH 15/20] feat(tasks): add a filter for tasks in service-details view --- app/components/service/includes/tasks.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/components/service/includes/tasks.html b/app/components/service/includes/tasks.html index e182e4ad3..3dd131b9e 100644 --- a/app/components/service/includes/tasks.html +++ b/app/components/service/includes/tasks.html @@ -12,6 +12,11 @@
+ +
+ +
+
@@ -48,7 +53,7 @@ - + From 79121f9977c1a8726dbb65ff1e37e4e56778654f Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 4 Oct 2017 08:38:55 +0200 Subject: [PATCH 16/20] docs(swagger): add missing Username field in UserAdminInitRequest --- api/swagger.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/swagger.yaml b/api/swagger.yaml index 6a69707c5..3c220c83b 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2569,6 +2569,10 @@ definitions: UserAdminInitRequest: type: "object" properties: + Username: + type: "string" + example: "admin" + description: "Username for the admin user" Password: type: "string" example: "admin-password" From f3a1250b274b9f7838157a459ef318c99efd0e3d Mon Sep 17 00:00:00 2001 From: Thomas Krzero Date: Wed, 4 Oct 2017 08:39:59 +0200 Subject: [PATCH 17/20] feat(container-creation) - Add container resource management (#1224) --- .../createContainerController.js | 63 +++++++++++++++++- .../createContainer/createcontainer.html | 66 +++++++++++++++++-- .../createService/createServiceController.js | 37 +++++++---- .../includes/resources-placement.html | 26 +++----- app/directives/slider/porSliderController.js | 1 + 5 files changed, 157 insertions(+), 36 deletions(-) diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index 2906f81b5..ebc5255c8 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createContainer', []) -.controller('CreateContainerController', ['$q', '$scope', '$state', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SettingsService', -function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SettingsService) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$timeout', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SystemService', 'SettingsService', +function ($q, $scope, $state, $timeout, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SystemService, SettingsService) { $scope.formValues = { alwaysPull: true, @@ -13,13 +13,22 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, ExtraHosts: [], IPv4: '', IPv6: '', - AccessControlData: new AccessControlFormData() + AccessControlData: new AccessControlFormData(), + CpuLimit: 0, + MemoryLimit: 0, + MemoryReservation: 0 }; $scope.state = { formValidationError: '' }; + $scope.refreshSlider = function () { + $timeout(function () { + $scope.$broadcast('rzSliderForceRender'); + }); + }; + $scope.config = { Image: '', Env: [], @@ -221,6 +230,25 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, config.HostConfig.Devices = path; } + function prepareResources(config) { + // Memory Limit - Round to 0.125 + var memoryLimit = (Math.round($scope.formValues.MemoryLimit * 8) / 8).toFixed(3); + memoryLimit *= 1024 * 1024; + if (memoryLimit > 0) { + config.HostConfig.Memory = memoryLimit; + } + // Memory Resevation - Round to 0.125 + var memoryReservation = (Math.round($scope.formValues.MemoryReservation * 8) / 8).toFixed(3); + memoryReservation *= 1024 * 1024; + if (memoryReservation > 0) { + config.HostConfig.MemoryReservation = memoryReservation; + } + // CPU Limit + if ($scope.formValues.CpuLimit > 0) { + config.HostConfig.NanoCpus = $scope.formValues.CpuLimit * 1000000000; + } + } + function prepareConfiguration() { var config = angular.copy($scope.config); config.Cmd = ContainerHelper.commandStringToArray(config.Cmd); @@ -232,6 +260,7 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, prepareVolumes(config); prepareLabels(config); prepareDevices(config); + prepareResources(config); return config; } @@ -416,6 +445,18 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, }); } + function loadFromContainerResources(d) { + if (d.HostConfig.NanoCpus) { + $scope.formValues.CpuLimit = d.HostConfig.NanoCpus / 1000000000; + } + if (d.HostConfig.Memory) { + $scope.formValues.MemoryLimit = d.HostConfig.Memory / 1024 / 1024; + } + if (d.HostConfig.MemoryReservation) { + $scope.formValues.MemoryReservation = d.HostConfig.MemoryReservation / 1024 / 1024; + } + } + function loadFromContainerSpec() { // Get container Container.get({ id: $transition$.params().from }).$promise @@ -435,6 +476,7 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, loadFromContainerConsole(d); loadFromContainerDevices(d); loadFromContainerImageConfig(d); + loadFromContainerResources(d); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve container'); @@ -482,6 +524,21 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerHelper, Notifications.error('Failure', e, 'Unable to retrieve running containers'); }); + SystemService.info() + .then(function success(data) { + $scope.state.sliderMaxCpu = 32; + if (data.NCPU) { + $scope.state.sliderMaxCpu = data.NCPU; + } + $scope.state.sliderMaxMemory = 32768; + if (data.MemTotal) { + $scope.state.sliderMaxMemory = Math.floor(data.MemTotal / 1000 / 1000); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve engine details'); + }); + SettingsService.publicSettings() .then(function success(data) { $scope.allowBindMounts = data.AllowBindMountsForRegularUsers; diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index 2701fe7d6..c784d7517 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -141,7 +141,7 @@
  • Env
  • Labels
  • Restart policy
  • -
  • Runtime
  • +
  • Runtime & Resources
  • @@ -466,9 +466,12 @@
    - -
    + +
    +
    + Runtime +
    @@ -510,10 +513,63 @@
    +
    + Resources +
    + +
    + +
    + +
    +
    + +
    +
    +

    + Memory soft limit (MB) +

    +
    +
    + + +
    + +
    + +
    +
    + +
    +
    +

    + Memory limit (MB) +

    +
    +
    + + +
    + +
    + +
    +
    +

    + Maximum CPU usage +

    +
    +
    + -
    - +
    diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index 5b0cb0a62..887fe7cb3 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -352,6 +352,29 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, Se createNewService(config, accessControlData); }; + function initSlidersMaxValuesBasedOnNodeData(nodes) { + var maxCpus = 0; + var maxMemory = 0; + for (var n in nodes) { + if (nodes[n].CPUs && nodes[n].CPUs > maxCpus) { + maxCpus = nodes[n].CPUs; + } + if (nodes[n].Memory && nodes[n].Memory > maxMemory) { + maxMemory = nodes[n].Memory; + } + } + if (maxCpus > 0) { + $scope.state.sliderMaxCpu = maxCpus / 1000000000; + } else { + $scope.state.sliderMaxCpu = 32; + } + if (maxMemory > 0) { + $scope.state.sliderMaxMemory = Math.floor(maxMemory / 1000 / 1000); + } else { + $scope.state.sliderMaxMemory = 32768; + } + } + function initView() { $('#loadingViewSpinner').show(); var apiVersion = $scope.applicationState.endpoint.apiVersion; @@ -368,18 +391,8 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, Se $scope.availableVolumes = data.volumes; $scope.availableNetworks = data.networks; $scope.availableSecrets = data.secrets; - // Set max cpu value - var maxCpus = 0; - for (var n in data.nodes) { - if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) { - maxCpus = data.nodes[n].CPUs; - } - } - if (maxCpus > 0) { - $scope.state.sliderMaxCpu = maxCpus / 1000000000; - } else { - $scope.state.sliderMaxCpu = 32; - } + var nodes = data.nodes; + initSlidersMaxValuesBasedOnNodeData(nodes); var settings = data.settings; $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers; var userDetails = Authentication.getUserDetails(); diff --git a/app/components/createService/includes/resources-placement.html b/app/components/createService/includes/resources-placement.html index 6c8b85dd6..17fc9b6a9 100644 --- a/app/components/createService/includes/resources-placement.html +++ b/app/components/createService/includes/resources-placement.html @@ -4,42 +4,36 @@
    -
    -
    + + diff --git a/app/components/createStack/createStackController.js b/app/components/createStack/createStackController.js new file mode 100644 index 000000000..840b81e8c --- /dev/null +++ b/app/components/createStack/createStackController.js @@ -0,0 +1,119 @@ +angular.module('createStack', []) +.controller('CreateStackController', ['$scope', '$state', '$document', 'StackService', 'CodeMirrorService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', +function ($scope, $state, $document, StackService, CodeMirrorService, Authentication, Notifications, FormValidator, ResourceControlService) { + + // Store the editor content when switching builder methods + var editorContent = ''; + var editorEnabled = true; + + $scope.formValues = { + Name: '', + StackFileContent: '# Define or paste the content of your docker-compose file here', + StackFile: null, + RepositoryURL: '', + RepositoryPath: 'docker-compose.yml', + AccessControlData: new AccessControlFormData() + }; + + $scope.state = { + Method: 'editor', + formValidationError: '' + }; + + function validateForm(accessControlData, isAdmin) { + $scope.state.formValidationError = ''; + var error = ''; + error = FormValidator.validateAccessControl(accessControlData, isAdmin); + + if (error) { + $scope.state.formValidationError = error; + return false; + } + return true; + } + + function createStack(name) { + var method = $scope.state.Method; + + if (method === 'editor') { + // The codemirror editor does not work with ng-model so we need to retrieve + // the value directly from the editor. + var stackFileContent = $scope.editor.getValue(); + + return StackService.createStackFromFileContent(name, stackFileContent); + } else if (method === 'upload') { + var stackFile = $scope.formValues.StackFile; + return StackService.createStackFromFileUpload(name, stackFile); + } else if (method === 'repository') { + var gitRepository = $scope.formValues.RepositoryURL; + var pathInRepository = $scope.formValues.RepositoryPath; + return StackService.createStackFromGitRepository(name, gitRepository, pathInRepository); + } + } + + $scope.deployStack = function () { + $('#createResourceSpinner').show(); + + var name = $scope.formValues.Name; + + var accessControlData = $scope.formValues.AccessControlData; + var userDetails = Authentication.getUserDetails(); + var isAdmin = userDetails.role === 1 ? true : false; + var userId = userDetails.ID; + + if (!validateForm(accessControlData, isAdmin)) { + $('#createResourceSpinner').hide(); + return; + } + + createStack(name) + .then(function success(data) { + Notifications.success('Stack successfully deployed'); + }) + .catch(function error(err) { + Notifications.warning('Deployment error', err.err.data.err); + }) + .then(function success(data) { + return ResourceControlService.applyResourceControl('stack', name, userId, accessControlData, []); + }) + .then(function success() { + $state.go('stacks'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to apply resource control on the stack'); + }) + .finally(function final() { + $('#createResourceSpinner').hide(); + }); + }; + + function enableEditor(value) { + $document.ready(function() { + var webEditorElement = $document[0].getElementById('web-editor'); + if (webEditorElement) { + $scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement); + if (value) { + $scope.editor.setValue(value); + } + } + }); + } + + $scope.toggleEditor = function() { + if (!editorEnabled) { + enableEditor(editorContent); + editorEnabled = true; + } + }; + + $scope.saveEditorContent = function() { + editorContent = $scope.editor.getValue(); + editorEnabled = false; + }; + + function initView() { + enableEditor(); + } + + initView(); +}]); diff --git a/app/components/createStack/createstack.html b/app/components/createStack/createstack.html new file mode 100644 index 000000000..4845e58e3 --- /dev/null +++ b/app/components/createStack/createstack.html @@ -0,0 +1,156 @@ + + + + + + Stacks > Add stack + + + +
    +
    + + +
    + +
    + +
    + +
    +
    + +
    + + This stack will be deployed using the equivalent of the docker stack deploy command. + +
    + +
    + Build method +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + Web editor +
    +
    + + You can get more information about Compose file format in the official documentation. + +
    +
    +
    + +
    +
    +
    + + +
    +
    + Upload +
    +
    + + You can upload a Compose file from your computer. + +
    +
    +
    + + + {{ formValues.StackFile.name }} + + +
    +
    +
    + + +
    +
    + Git repository +
    +
    + + You can use the URL of a public git repository. + +
    +
    + +
    + +
    +
    +
    + + Indicate the path to the Compose file from the root of your repository. + +
    +
    + +
    + +
    +
    +
    + + + +
    + Actions +
    +
    +
    + + Cancel + + {{ state.formValidationError }} +
    +
    + + +
    +
    +
    +
    diff --git a/app/components/dashboard/dashboard.html b/app/components/dashboard/dashboard.html index d3b1426c5..ebc2f7501 100644 --- a/app/components/dashboard/dashboard.html +++ b/app/components/dashboard/dashboard.html @@ -85,6 +85,32 @@
    + +
    diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js index c9598209c..4f9d83094 100644 --- a/app/components/dashboard/dashboardController.js +++ b/app/components/dashboard/dashboardController.js @@ -1,6 +1,6 @@ angular.module('dashboard', []) -.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'SystemService', 'Notifications', -function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, SystemService, Notifications) { +.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'SystemService', 'ServiceService', 'StackService', 'Notifications', +function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, SystemService, ServiceService, StackService, Notifications) { $scope.containerData = { total: 0 @@ -15,6 +15,9 @@ function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, System total: 0 }; + $scope.serviceCount = 0; + $scope.stackCount = 0; + function prepareContainerData(d) { var running = 0; var stopped = 0; @@ -63,18 +66,25 @@ function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, System function initView() { $('#loadingViewSpinner').show(); + + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + $q.all([ Container.query({all: 1}).$promise, Image.query({}).$promise, Volume.query({}).$promise, Network.query({}).$promise, - SystemService.info() + SystemService.info(), + endpointProvider === 'DOCKER_SWARM_MODE' ? ServiceService.services() : [], + endpointProvider === 'DOCKER_SWARM_MODE' ? StackService.stacks(true) : [] ]).then(function (d) { prepareContainerData(d[0]); prepareImageData(d[1]); prepareVolumeData(d[2]); prepareNetworkData(d[3]); prepareInfoData(d[4]); + $scope.serviceCount = d[5].length; + $scope.stackCount = d[6].length; $('#loadingViewSpinner').hide(); }, function(e) { $('#loadingViewSpinner').hide(); diff --git a/app/components/networks/networks.html b/app/components/networks/networks.html index 4773aa779..59c64627d 100644 --- a/app/components/networks/networks.html +++ b/app/components/networks/networks.html @@ -48,10 +48,10 @@
    - - + + diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index cf54ae214..9ff2cb6e2 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -321,7 +321,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, originalService = angular.copy(service); return $q.all({ - tasks: TaskService.serviceTasks(service.Name), + tasks: TaskService.tasks({ service: [service.Name] }), nodes: NodeService.nodes(), secrets: apiVersion >= 1.25 ? SecretService.secrets() : [] }); diff --git a/app/components/services/services.html b/app/components/services/services.html index 3b373937a..2ff52b79a 100644 --- a/app/components/services/services.html +++ b/app/components/services/services.html @@ -38,42 +38,49 @@ + + + + + @@ -54,7 +61,11 @@ - + diff --git a/app/directives/serviceList/por-service-list.js b/app/directives/serviceList/por-service-list.js new file mode 100644 index 000000000..4a33ba96e --- /dev/null +++ b/app/directives/serviceList/por-service-list.js @@ -0,0 +1,8 @@ +angular.module('portainer').component('porServiceList', { + templateUrl: 'app/directives/serviceList/porServiceList.html', + controller: 'porServiceListController', + bindings: { + 'services': '<', + 'nodes': '<' + } +}); diff --git a/app/directives/serviceList/porServiceList.html b/app/directives/serviceList/porServiceList.html new file mode 100644 index 000000000..e87f74d15 --- /dev/null +++ b/app/directives/serviceList/porServiceList.html @@ -0,0 +1,98 @@ +
    +
    + + +
    + Items per page: + +
    +
    + +
    + +
    +
    + +
    +
    {{ task.Id }} {{ task.Status.State }} {{ task.Slot }} - + State - + Name - - - + + + - + + Stack + + + + + Image - + IP Address - + Host IP - + Published Ports - + Ownership @@ -111,6 +118,7 @@ {{ container|swarmcontainername|truncate: truncate_size}} {{ container|containername|truncate: truncate_size}}{{ container.StackName ? container.StackName : '-' }} {{ container.Image | hideshasum }} {{ container.IP ? container.IP : '-' }} {{ container.hostIP }} - - Id - - + + Stack + + @@ -101,8 +101,8 @@
    {{ network.Name|truncate:40}}{{ network.Id|truncate:20 }}{{ network.Name | truncate:40 }}{{ network.StackName ? network.StackName : '-' }} {{ network.Scope }} {{ network.Driver }} {{ network.IPAM.Driver }}
    - + Name - + + Stack + + + + + Image - + Scheduling mode - + Published Ports - + Updated at - + Ownership @@ -84,6 +91,7 @@
    {{ service.Name }}{{ service.StackName ? service.StackName : '-' }} {{ service.Image | hideshasum }} {{ service.Mode }} diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index b85bb2c64..da646ae77 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -25,6 +25,9 @@ LinuxServer.io + diff --git a/app/components/stack/stack.html b/app/components/stack/stack.html new file mode 100644 index 000000000..8795ce307 --- /dev/null +++ b/app/components/stack/stack.html @@ -0,0 +1,54 @@ + + + + + + + + + Stacks > {{ stack.Name }} + + + + + + + + + + + +
    +
    + + + +
    +
    + + You can get more information about Compose file format in the official documentation. + +
    +
    +
    + +
    +
    +
    + Actions +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    diff --git a/app/components/stack/stackController.js b/app/components/stack/stackController.js new file mode 100644 index 000000000..b483c1cc7 --- /dev/null +++ b/app/components/stack/stackController.js @@ -0,0 +1,78 @@ +angular.module('stack', []) +.controller('StackController', ['$q', '$scope', '$state', '$stateParams', '$document', 'StackService', 'NodeService', 'ServiceService', 'TaskService', 'ServiceHelper', 'CodeMirrorService', 'Notifications', +function ($q, $scope, $state, $stateParams, $document, StackService, NodeService, ServiceService, TaskService, ServiceHelper, CodeMirrorService, Notifications) { + + $scope.deployStack = function () { + $('#createResourceSpinner').show(); + + // The codemirror editor does not work with ng-model so we need to retrieve + // the value directly from the editor. + var stackFile = $scope.editor.getValue(); + + StackService.updateStack($scope.stack.Id, stackFile) + .then(function success(data) { + Notifications.success('Stack successfully deployed'); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create stack'); + }) + .finally(function final() { + $('#createResourceSpinner').hide(); + }); + }; + + function initView() { + $('#loadingViewSpinner').show(); + var stackId = $stateParams.id; + + StackService.stack(stackId) + .then(function success(data) { + var stack = data; + $scope.stack = stack; + + var serviceFilters = { + label: ['com.docker.stack.namespace=' + stack.Name] + }; + + return $q.all({ + stackFile: StackService.getStackFile(stackId), + services: ServiceService.services(serviceFilters), + tasks: TaskService.tasks(serviceFilters), + nodes: NodeService.nodes() + }); + }) + .then(function success(data) { + $scope.stackFileContent = data.stackFile; + + $document.ready(function() { + var webEditorElement = $document[0].getElementById('web-editor'); + if (webEditorElement) { + $scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement); + } + }); + + $scope.nodes = data.nodes; + + var services = data.services; + + var tasks = data.tasks; + $scope.tasks = tasks; + + for (var i = 0; i < services.length; i++) { + var service = services[i]; + ServiceHelper.associateTasksToService(service, tasks); + } + + $scope.services = services; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve tasks details'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); +}]); diff --git a/app/components/stacks/stacks.html b/app/components/stacks/stacks.html new file mode 100644 index 000000000..8d585eea8 --- /dev/null +++ b/app/components/stacks/stacks.html @@ -0,0 +1,120 @@ + + + + + + + + Stacks + + +
    +
    + + + +
    +
    + + Stacks marked with the icon are external stacks that were created outside of Portainer. You'll not be able to execute any actions against these stacks. + +
    +
    + Filters +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    + Items per page: + +
    +
    + +
    + + Add stack +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + +
    + + + + Name + + + + + + Ownership + + + +
    + + {{ stack.Name }} + + + {{ stack.Name }} + + + + + {{ stack.ResourceControl.Ownership ? stack.ResourceControl.Ownership : stack.ResourceControl.Ownership = 'public' }} + +
    Loading...
    No stacks available.
    +
    + +
    +
    +
    + +
    +
    diff --git a/app/components/stacks/stacksController.js b/app/components/stacks/stacksController.js new file mode 100644 index 000000000..352891acd --- /dev/null +++ b/app/components/stacks/stacksController.js @@ -0,0 +1,103 @@ +angular.module('stacks', []) +.controller('StacksController', ['$scope', 'Notifications', 'Pagination', 'StackService', 'ModalService', +function ($scope, Notifications, Pagination, StackService, ModalService) { + $scope.state = {}; + $scope.state.selectedItemCount = 0; + $scope.state.pagination_count = Pagination.getPaginationCount('stacks'); + $scope.sortType = 'Name'; + $scope.sortReverse = false; + $scope.state.DisplayInformationPanel = false; + $scope.state.DisplayExternalStacks = true; + + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('stacks', $scope.state.pagination_count); + }; + + $scope.order = function (sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; + + $scope.selectItems = function (allSelected) { + angular.forEach($scope.state.filteredStacks, function (stack) { + if (stack.Id && stack.Checked !== allSelected) { + stack.Checked = allSelected; + $scope.selectItem(stack); + } + }); + }; + + $scope.selectItem = function (item) { + if (item.Checked) { + $scope.state.selectedItemCount++; + } else { + $scope.state.selectedItemCount--; + } + }; + + $scope.removeAction = function () { + ModalService.confirmDeletion( + 'Do you want to remove the selected stack(s)? Associated services will be removed as well.', + function onConfirm(confirmed) { + if(!confirmed) { return; } + deleteSelectedStacks(); + } + ); + }; + + function deleteSelectedStacks() { + $('#loadingViewSpinner').show(); + var counter = 0; + + var complete = function () { + counter = counter - 1; + if (counter === 0) { + $('#loadingViewSpinner').hide(); + } + }; + + angular.forEach($scope.stacks, function (stack) { + if (stack.Checked) { + counter = counter + 1; + StackService.remove(stack) + .then(function success() { + Notifications.success('Stack deleted', stack.Name); + var index = $scope.stacks.indexOf(stack); + $scope.stacks.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name); + }) + .finally(function final() { + complete(); + }); + } + }); + } + + function initView() { + $('#loadingViewSpinner').show(); + + StackService.stacks(true) + .then(function success(data) { + var stacks = data; + for (var i = 0; i < stacks.length; i++) { + var stack = stacks[i]; + if (stack.External) { + $scope.state.DisplayInformationPanel = true; + break; + } + } + $scope.stacks = stacks; + }) + .catch(function error(err) { + $scope.stacks = []; + Notifications.error('Failure', err, 'Unable to retrieve stacks'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); +}]); diff --git a/app/components/volumes/volumes.html b/app/components/volumes/volumes.html index c90e40945..fb9dd94b0 100644 --- a/app/components/volumes/volumes.html +++ b/app/components/volumes/volumes.html @@ -57,6 +57,13 @@ +
    + + Stack + + + + Driver @@ -87,6 +94,7 @@ {{ volume.Id|truncate:25 }} Unused + {{ volume.StackName ? volume.StackName : '-' }} {{ volume.Driver }} {{ volume.Mountpoint | truncatelr }} diff --git a/app/directives/accessControlPanel/porAccessControlPanel.html b/app/directives/accessControlPanel/porAccessControlPanel.html index 221b18515..e9fd62cd2 100644 --- a/app/directives/accessControlPanel/porAccessControlPanel.html +++ b/app/directives/accessControlPanel/porAccessControlPanel.html @@ -37,6 +37,13 @@
    + + Access control on this resource is inherited from the following stack: {{ $ctrl.resourceControl.ResourceId }} + +
    Authorized users
    Change ownership
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + + Name + + + + + + Image + + + + + + Scheduling mode + + + + + + Published Ports + + + + + + Updated at + + + +
    {{ service.Name }}{{ service.Image | hideshasum }} + {{ service.Mode }} + {{ service.Tasks | runningtaskscount }} + / + {{ service.Mode === 'replicated' ? service.Replicas : ($ctrl.nodes | availablenodecount) }} + + + {{ p.PublishedPort }}:{{ p.TargetPort }} + + - + + {{ service.UpdatedAt|getisodate }} +
    Loading...
    No services available.
    +
    + +
    +
    + + +
    +
    diff --git a/app/directives/serviceList/porServiceList.js b/app/directives/serviceList/porServiceList.js new file mode 100644 index 000000000..b755ef70c --- /dev/null +++ b/app/directives/serviceList/porServiceList.js @@ -0,0 +1,21 @@ +angular.module('portainer') +.controller('porServiceListController', ['EndpointProvider', 'Pagination', +function (EndpointProvider, Pagination) { + var ctrl = this; + ctrl.state = { + pagination_count: Pagination.getPaginationCount('services_list'), + publicURL: EndpointProvider.endpointPublicURL() + }; + ctrl.sortType = 'Name'; + ctrl.sortReverse = false; + + ctrl.order = function(sortType) { + ctrl.sortReverse = (ctrl.sortType === sortType) ? !ctrl.sortReverse : false; + ctrl.sortType = sortType; + }; + + ctrl.changePaginationCount = function() { + Pagination.setPaginationCount('services_list', ctrl.state.pagination_count); + }; + +}]); diff --git a/app/directives/taskList/por-task-list.js b/app/directives/taskList/por-task-list.js new file mode 100644 index 000000000..6ed1483d3 --- /dev/null +++ b/app/directives/taskList/por-task-list.js @@ -0,0 +1,8 @@ +angular.module('portainer').component('porTaskList', { + templateUrl: 'app/directives/taskList/porTaskList.html', + controller: 'porTaskListController', + bindings: { + 'tasks': '<', + 'nodes': '<' + } +}); diff --git a/app/directives/taskList/porTaskList.html b/app/directives/taskList/porTaskList.html new file mode 100644 index 000000000..c2c75d7da --- /dev/null +++ b/app/directives/taskList/porTaskList.html @@ -0,0 +1,78 @@ +
    +
    + + +
    + Items per page: + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Id + + Status + + + + + + Slot + + + + + + Node + + + + + + Last update + + + +
    {{ task.Id }}{{ task.Status.State }}{{ task.Slot ? task.Slot : '-' }}{{ task.NodeId | tasknodename: $ctrl.nodes }}{{ task.Updated | getisodate }}
    Loading...
    No tasks available.
    +
    + +
    +
    +
    +
    +
    diff --git a/app/directives/taskList/porTaskList.js b/app/directives/taskList/porTaskList.js new file mode 100644 index 000000000..e53f546d0 --- /dev/null +++ b/app/directives/taskList/porTaskList.js @@ -0,0 +1,19 @@ +angular.module('portainer') +.controller('porTaskListController', ['Pagination', +function (Pagination) { + var ctrl = this; + ctrl.state = { + pagination_count: Pagination.getPaginationCount('tasks_list') + }; + ctrl.sortType = 'Updated'; + ctrl.sortReverse = true; + + ctrl.order = function(sortType) { + ctrl.sortReverse = (ctrl.sortType === sortType) ? !ctrl.sortReverse : false; + ctrl.sortType = sortType; + }; + + ctrl.changePaginationCount = function() { + Pagination.setPaginationCount('tasks_list', ctrl.state.pagination_count); + }; +}]); diff --git a/app/filters/filters.js b/app/filters/filters.js index e1d61bd95..efc463d23 100644 --- a/app/filters/filters.js +++ b/app/filters/filters.js @@ -75,7 +75,7 @@ angular.module('portainer.filters', []) return 'warning'; } else if (includeString(status, ['created'])) { return 'info'; - } else if (includeString(status, ['stopped', 'unhealthy', 'dead'])) { + } else if (includeString(status, ['stopped', 'unhealthy', 'dead', 'exited'])) { return 'danger'; } return 'success'; @@ -298,6 +298,32 @@ angular.module('portainer.filters', []) } }; }) +.filter('availablenodecount', function () { + 'use strict'; + return function (nodes) { + var availableNodes = 0; + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (node.Availability === 'active' && node.Status === 'ready') { + availableNodes++; + } + } + return availableNodes; + }; +}) +.filter('runningtaskscount', function () { + 'use strict'; + return function (tasks) { + var runningTasks = 0; + for (var i = 0; i < tasks.length; i++) { + var task = tasks[i]; + if (task.Status.State === 'running') { + runningTasks++; + } + } + return runningTasks; + }; +}) .filter('tasknodename', function () { 'use strict'; return function (nodeId, nodes) { diff --git a/app/helpers/serviceHelper.js b/app/helpers/serviceHelper.js index ec3411421..00d063278 100644 --- a/app/helpers/serviceHelper.js +++ b/app/helpers/serviceHelper.js @@ -1,123 +1,145 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHelperFactory() { 'use strict'; - return { - serviceToConfig: function(service) { - return { - Name: service.Spec.Name, - Labels: service.Spec.Labels, - TaskTemplate: service.Spec.TaskTemplate, - Mode: service.Spec.Mode, - UpdateConfig: service.Spec.UpdateConfig, - Networks: service.Spec.Networks, - EndpointSpec: service.Spec.EndpointSpec - }; - }, - translateKeyValueToPlacementPreferences: function(keyValuePreferences) { - if (keyValuePreferences) { - var preferences = []; - keyValuePreferences.forEach(function(preference) { - if (preference.strategy && preference.strategy !== '' && preference.value && preference.value !== '') { - switch (preference.strategy.toLowerCase()) { - case 'spread': - preferences.push({ - 'Spread': { - 'SpreadDescriptor': preference.value - } - }); - break; - } + + var helper = {}; + + helper.associateTasksToService = function(service, tasks) { + service.Tasks = []; + var otherServicesTasks = []; + for (var i = 0; i < tasks.length; i++) { + var task = tasks[i]; + if (task.ServiceId === service.Id) { + service.Tasks.push(task); + } else { + otherServicesTasks.push(task); + } + } + tasks = otherServicesTasks; + }; + + helper.serviceToConfig = function(service) { + return { + Name: service.Spec.Name, + Labels: service.Spec.Labels, + TaskTemplate: service.Spec.TaskTemplate, + Mode: service.Spec.Mode, + UpdateConfig: service.Spec.UpdateConfig, + Networks: service.Spec.Networks, + EndpointSpec: service.Spec.EndpointSpec + }; + }; + + helper.translateKeyValueToPlacementPreferences = function(keyValuePreferences) { + if (keyValuePreferences) { + var preferences = []; + keyValuePreferences.forEach(function(preference) { + if (preference.strategy && preference.strategy !== '' && preference.value && preference.value !== '') { + switch (preference.strategy.toLowerCase()) { + case 'spread': + preferences.push({ + 'Spread': { + 'SpreadDescriptor': preference.value + } + }); + break; } - }); - return preferences; - } - return []; - }, - translateKeyValueToPlacementConstraints: function(keyValueConstraints) { - if (keyValueConstraints) { - var constraints = []; - keyValueConstraints.forEach(function(constraint) { - if (constraint.key && constraint.key !== '' && constraint.value && constraint.value !== '') { - constraints.push(constraint.key + constraint.operator + constraint.value); - } - }); - return constraints; - } - return []; - }, - translateEnvironmentVariables: function(env) { - if (env) { - var variables = []; - env.forEach(function(variable) { - var idx = variable.indexOf('='); - var keyValue = [variable.slice(0, idx), variable.slice(idx + 1)]; - var originalValue = (keyValue.length > 1) ? keyValue[1] : ''; - variables.push({ - key: keyValue[0], - value: originalValue, - originalKey: keyValue[0], - originalValue: originalValue, - added: true - }); - }); - return variables; - } - return []; - }, - translateEnvironmentVariablesToEnv: function(env) { - if (env) { - var variables = []; - env.forEach(function(variable) { - if (variable.key && variable.key !== '') { - variables.push(variable.key + '=' + variable.value); - } - }); - return variables; - } - return []; - }, - translatePreferencesToKeyValue: function(preferences) { - if (preferences) { - var keyValuePreferences = []; - preferences.forEach(function(preference) { - if (preference.Spread) { - keyValuePreferences.push({ - strategy: 'Spread', - value: preference.Spread.SpreadDescriptor - }); - } - }); - return keyValuePreferences; - } - return []; - }, - translateConstraintsToKeyValue: function(constraints) { - function getOperator(constraint) { - var indexEquals = constraint.indexOf('=='); - if (indexEquals >= 0) { - return [indexEquals, '==']; } - return [constraint.indexOf('!='), '!=']; - } - if (constraints) { - var keyValueConstraints = []; - constraints.forEach(function(constraint) { - var operatorIndices = getOperator(constraint); + }); + return preferences; + } + return []; + }; - var key = constraint.slice(0, operatorIndices[0]); - var operator = operatorIndices[1]; - var value = constraint.slice(operatorIndices[0] + 2); + helper.translateKeyValueToPlacementConstraints = function(keyValueConstraints) { + if (keyValueConstraints) { + var constraints = []; + keyValueConstraints.forEach(function(constraint) { + if (constraint.key && constraint.key !== '' && constraint.value && constraint.value !== '') { + constraints.push(constraint.key + constraint.operator + constraint.value); + } + }); + return constraints; + } + return []; + }; - keyValueConstraints.push({ - key: key, - value: value, - operator: operator, - originalKey: key, - originalValue: value - }); + helper.translateEnvironmentVariables = function(env) { + if (env) { + var variables = []; + env.forEach(function(variable) { + var idx = variable.indexOf('='); + var keyValue = [variable.slice(0, idx), variable.slice(idx + 1)]; + var originalValue = (keyValue.length > 1) ? keyValue[1] : ''; + variables.push({ + key: keyValue[0], + value: originalValue, + originalKey: keyValue[0], + originalValue: originalValue, + added: true }); - return keyValueConstraints; + }); + return variables; + } + return []; + }; + + helper.translateEnvironmentVariablesToEnv = function(env) { + if (env) { + var variables = []; + env.forEach(function(variable) { + if (variable.key && variable.key !== '') { + variables.push(variable.key + '=' + variable.value); + } + }); + return variables; + } + return []; + }; + + helper.translatePreferencesToKeyValue = function(preferences) { + if (preferences) { + var keyValuePreferences = []; + preferences.forEach(function(preference) { + if (preference.Spread) { + keyValuePreferences.push({ + strategy: 'Spread', + value: preference.Spread.SpreadDescriptor + }); + } + }); + return keyValuePreferences; + } + return []; + }; + + helper.translateConstraintsToKeyValue = function(constraints) { + function getOperator(constraint) { + var indexEquals = constraint.indexOf('=='); + if (indexEquals >= 0) { + return [indexEquals, '==']; } - return []; + return [constraint.indexOf('!='), '!=']; + } + if (constraints) { + var keyValueConstraints = []; + constraints.forEach(function(constraint) { + var operatorIndices = getOperator(constraint); + + var key = constraint.slice(0, operatorIndices[0]); + var operator = operatorIndices[1]; + var value = constraint.slice(operatorIndices[0] + 2); + + keyValueConstraints.push({ + key: key, + value: value, + operator: operator, + originalKey: key, + originalValue: value + }); + }); + return keyValueConstraints; } }; + + return helper; }]); diff --git a/app/helpers/stackHelper.js b/app/helpers/stackHelper.js new file mode 100644 index 000000000..8701d9d4f --- /dev/null +++ b/app/helpers/stackHelper.js @@ -0,0 +1,21 @@ +angular.module('portainer.helpers') +.factory('StackHelper', [function StackHelperFactory() { + 'use strict'; + var helper = {}; + + helper.getExternalStackNamesFromServices = function(services) { + var stackNames = []; + + for (var i = 0; i < services.length; i++) { + var service = services[i]; + if (!service.Labels || !service.Labels['com.docker.stack.namespace']) continue; + + var stackName = service.Labels['com.docker.stack.namespace']; + stackNames.push(stackName); + } + + return _.uniq(stackNames); + }; + + return helper; +}]); diff --git a/app/models/api/stack.js b/app/models/api/stack.js new file mode 100644 index 000000000..3d4645913 --- /dev/null +++ b/app/models/api/stack.js @@ -0,0 +1,9 @@ +function StackViewModel(data) { + this.Id = data.Id; + this.Name = data.Name; + this.Checked = false; + if (data.ResourceControl && data.ResourceControl.Id !== 0) { + this.ResourceControl = new ResourceControlViewModel(data.ResourceControl); + } + this.External = data.External; +} diff --git a/app/models/docker/container.js b/app/models/docker/container.js index 552963b22..8c2d112f5 100644 --- a/app/models/docker/container.js +++ b/app/models/docker/container.js @@ -8,9 +8,15 @@ function ContainerViewModel(data) { this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress; } this.Image = data.Image; + this.ImageID = data.ImageID; this.Command = data.Command; this.Checked = false; this.Labels = data.Labels; + if (this.Labels && this.Labels['com.docker.compose.project']) { + this.StackName = this.Labels['com.docker.compose.project']; + } else if (this.Labels && this.Labels['com.docker.stack.namespace']) { + this.StackName = this.Labels['com.docker.stack.namespace']; + } this.Mounts = data.Mounts; this.Ports = []; diff --git a/app/models/docker/network.js b/app/models/docker/network.js index 820b35ab6..baa5ab984 100644 --- a/app/models/docker/network.js +++ b/app/models/docker/network.js @@ -8,6 +8,13 @@ function NetworkViewModel(data) { this.Containers = data.Containers; this.Options = data.Options; + this.Labels = data.Labels; + if (this.Labels && this.Labels['com.docker.compose.project']) { + this.StackName = this.Labels['com.docker.compose.project']; + } else if (this.Labels && this.Labels['com.docker.stack.namespace']) { + this.StackName = this.Labels['com.docker.stack.namespace']; + } + if (data.Portainer) { if (data.Portainer.ResourceControl) { this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); diff --git a/app/models/docker/service.js b/app/models/docker/service.js index 3c58cd9e3..28d8609ba 100644 --- a/app/models/docker/service.js +++ b/app/models/docker/service.js @@ -1,6 +1,7 @@ function ServiceViewModel(data, runningTasks, nodes) { this.Model = data; this.Id = data.ID; + this.Tasks = []; this.Name = data.Spec.Name; this.CreatedAt = data.CreatedAt; this.UpdatedAt = data.UpdatedAt; @@ -44,6 +45,9 @@ function ServiceViewModel(data, runningTasks, nodes) { this.Preferences = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Preferences || [] : []; this.Platforms = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Platforms || [] : []; this.Labels = data.Spec.Labels; + if (this.Labels && this.Labels['com.docker.stack.namespace']) { + this.StackName = this.Labels['com.docker.stack.namespace']; + } var containerSpec = data.Spec.TaskTemplate.ContainerSpec; if (containerSpec) { diff --git a/app/models/docker/swarm.js b/app/models/docker/swarm.js new file mode 100644 index 000000000..9ed805335 --- /dev/null +++ b/app/models/docker/swarm.js @@ -0,0 +1,3 @@ +function SwarmViewModel(data) { + this.Id = data.ID; +} diff --git a/app/models/docker/volume.js b/app/models/docker/volume.js index fc6dbd848..5e2701b70 100644 --- a/app/models/docker/volume.js +++ b/app/models/docker/volume.js @@ -3,6 +3,11 @@ function VolumeViewModel(data) { this.Driver = data.Driver; this.Options = data.Options; this.Labels = data.Labels; + if (this.Labels && this.Labels['com.docker.compose.project']) { + this.StackName = this.Labels['com.docker.compose.project']; + } else if (this.Labels && this.Labels['com.docker.stack.namespace']) { + this.StackName = this.Labels['com.docker.stack.namespace']; + } this.Mountpoint = data.Mountpoint; if (data.Portainer) { diff --git a/app/rest/api/stack.js b/app/rest/api/stack.js new file mode 100644 index 000000000..6512c5128 --- /dev/null +++ b/app/rest/api/stack.js @@ -0,0 +1,15 @@ +angular.module('portainer.rest') +.factory('Stack', ['$resource', 'EndpointProvider', 'API_ENDPOINT_ENDPOINTS', function StackFactory($resource, EndpointProvider, API_ENDPOINT_ENDPOINTS) { + 'use strict'; + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/stacks/:id/:action', { + endpointId: EndpointProvider.endpointID + }, + { + get: { method: 'GET', params: { id: '@id' } }, + query: { method: 'GET', isArray: true }, + create: { method: 'POST' }, + update: { method: 'PUT', params: { id: '@id' } }, + remove: { method: 'DELETE', params: { id: '@id'} }, + getStackFile: { method: 'GET', params: { id : '@id', action: 'stackfile' } } + }); +}]); diff --git a/app/rest/docker/service.js b/app/rest/docker/service.js index e8ca55962..466ded8e8 100644 --- a/app/rest/docker/service.js +++ b/app/rest/docker/service.js @@ -6,7 +6,7 @@ angular.module('portainer.rest') }, { get: { method: 'GET', params: {id: '@id'} }, - query: { method: 'GET', isArray: true }, + query: { method: 'GET', isArray: true, params: {filters: '@filters'} }, create: { method: 'POST', params: {action: 'create'}, headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader } diff --git a/app/routes.js b/app/routes.js index 15db71679..0232c3d6d 100644 --- a/app/routes.js +++ b/app/routes.js @@ -661,5 +661,44 @@ function configureRoutes($stateProvider) { controller: 'SidebarController' } } + }) + .state('actions.create.stack', { + url: '/stack', + views: { + 'content@': { + templateUrl: 'app/components/createStack/createstack.html', + controller: 'CreateStackController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('stacks', { + url: '/stacks/', + views: { + 'content@': { + templateUrl: 'app/components/stacks/stacks.html', + controller: 'StacksController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('stack', { + url: '^/stacks/:id/', + views: { + 'content@': { + templateUrl: 'app/components/stack/stack.html', + controller: 'StackController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } }); } diff --git a/app/services/api/stackService.js b/app/services/api/stackService.js new file mode 100644 index 000000000..6ab6e7f8a --- /dev/null +++ b/app/services/api/stackService.js @@ -0,0 +1,162 @@ +angular.module('portainer.services') +.factory('StackService', ['$q', 'Stack', 'ResourceControlService', 'FileUploadService', 'StackHelper', 'ServiceService', 'SwarmService', +function StackServiceFactory($q, Stack, ResourceControlService, FileUploadService, StackHelper, ServiceService, SwarmService) { + 'use strict'; + var service = {}; + + service.stack = function(id) { + var deferred = $q.defer(); + + Stack.get({ id: id }).$promise + .then(function success(data) { + var stack = new StackViewModel(data); + deferred.resolve(stack); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve stack details', err: err }); + }); + + return deferred.promise; + }; + + service.getStackFile = function(id) { + var deferred = $q.defer(); + + Stack.getStackFile({ id: id }).$promise + .then(function success(data) { + deferred.resolve(data.StackFileContent); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve stack content', err: err }); + }); + + return deferred.promise; + }; + + service.externalStacks = function() { + var deferred = $q.defer(); + + ServiceService.services() + .then(function success(data) { + var services = data; + var stackNames = StackHelper.getExternalStackNamesFromServices(services); + var stacks = stackNames.map(function (item) { + return new StackViewModel({ Name: item, External: true }); + }); + deferred.resolve(stacks); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve external stacks', err: err }); + }); + + return deferred.promise; + }; + + service.stacks = function(includeExternalStacks) { + var deferred = $q.defer(); + + SwarmService.swarm() + .then(function success(data) { + var swarm = data; + + return $q.all({ + stacks: Stack.query({ swarmId: swarm.Id }).$promise, + externalStacks: includeExternalStacks ? service.externalStacks() : [] + }); + }) + .then(function success(data) { + var stacks = data.stacks.map(function (item) { + item.External = false; + return new StackViewModel(item); + }); + var externalStacks = data.externalStacks; + + var result = _.unionWith(stacks, externalStacks, function(a, b) { return a.Name === b.Name; }); + deferred.resolve(result); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve stacks', err: err }); + }); + + return deferred.promise; + }; + + service.remove = function(stack) { + var deferred = $q.defer(); + + Stack.remove({ id: stack.Id }).$promise + .then(function success(data) { + if (stack.ResourceControl && stack.ResourceControl.Id) { + return ResourceControlService.deleteResourceControl(stack.ResourceControl.Id); + } + }) + .then(function success() { + deferred.resolve(); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove the stack', err: err }); + }); + + return deferred.promise; + }; + + service.createStackFromFileContent = function(name, stackFileContent) { + var deferred = $q.defer(); + + SwarmService.swarm() + .then(function success(data) { + var swarm = data; + return Stack.create({ method: 'string' }, { Name: name, SwarmID: swarm.Id, StackFileContent: stackFileContent }).$promise; + }) + .then(function success(data) { + deferred.resolve(data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create the stack', err: err }); + }); + + return deferred.promise; + }; + + service.createStackFromGitRepository = function(name, gitRepository, pathInRepository) { + var deferred = $q.defer(); + + SwarmService.swarm() + .then(function success(data) { + var swarm = data; + return Stack.create({ method: 'repository' }, { Name: name, SwarmID: swarm.Id, GitRepository: gitRepository, PathInRepository: pathInRepository }).$promise; + }) + .then(function success(data) { + deferred.resolve(data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create the stack', err: err }); + }); + + return deferred.promise; + }; + + service.createStackFromFileUpload = function(name, stackFile) { + var deferred = $q.defer(); + + SwarmService.swarm() + .then(function success(data) { + var swarm = data; + return FileUploadService.createStack(name, swarm.Id, stackFile); + }) + .then(function success(data) { + deferred.resolve(data.data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create the stack', err: err }); + }); + + return deferred.promise; + }; + + service.updateStack = function(id, stackFile) { + return Stack.update({ id: id, StackFileContent: stackFile }).$promise; + }; + + return service; +}]); diff --git a/app/services/codeMirror.js b/app/services/codeMirror.js new file mode 100644 index 000000000..b5807f87e --- /dev/null +++ b/app/services/codeMirror.js @@ -0,0 +1,21 @@ +angular.module('portainer.services') +.factory('CodeMirrorService', function CodeMirrorService() { + 'use strict'; + + var codeMirrorOptions = { + lineNumbers: true, + mode: 'text/x-yaml', + gutters: ['CodeMirror-lint-markers'], + lint: true + }; + + var service = {}; + + service.applyCodeMirrorOnElement = function(element) { + var cm = CodeMirror.fromTextArea(element, codeMirrorOptions); + cm.setSize('100%', 500); + return cm; + }; + + return service; +}); diff --git a/app/services/docker/containerService.js b/app/services/docker/containerService.js index c9f302d56..21bd1c295 100644 --- a/app/services/docker/containerService.js +++ b/app/services/docker/containerService.js @@ -18,16 +18,20 @@ angular.module('portainer.services') return deferred.promise; }; - service.containers = function(all) { + service.containers = function(all, filters) { var deferred = $q.defer(); - Container.query({ all: all }).$promise + + Container.query({ all: all, filters: filters ? filters : {} }).$promise .then(function success(data) { - var containers = data; + var containers = data.map(function (item) { + return new ContainerViewModel(item); + }); deferred.resolve(containers); }) .catch(function error(err) { deferred.reject({ msg: 'Unable to retrieve containers', err: err }); }); + return deferred.promise; }; diff --git a/app/services/docker/serviceService.js b/app/services/docker/serviceService.js index 0020c3412..65ad05709 100644 --- a/app/services/docker/serviceService.js +++ b/app/services/docker/serviceService.js @@ -1,12 +1,12 @@ angular.module('portainer.services') -.factory('ServiceService', ['$q', 'Service', 'ResourceControlService', function ServiceServiceFactory($q, Service, ResourceControlService) { +.factory('ServiceService', ['$q', 'Service', 'ServiceHelper', 'TaskService', 'ResourceControlService', function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, ResourceControlService) { 'use strict'; var service = {}; - service.services = function() { + service.services = function(filters) { var deferred = $q.defer(); - Service.query().$promise + Service.query({ filters: filters ? filters : {} }).$promise .then(function success(data) { var services = data.map(function (item) { return new ServiceViewModel(item); diff --git a/app/services/docker/swarmService.js b/app/services/docker/swarmService.js new file mode 100644 index 000000000..8a2dc3ff5 --- /dev/null +++ b/app/services/docker/swarmService.js @@ -0,0 +1,22 @@ +angular.module('portainer.services') +.factory('SwarmService', ['$q', 'Swarm', function SwarmServiceFactory($q, Swarm) { + 'use strict'; + var service = {}; + + service.swarm = function() { + var deferred = $q.defer(); + + Swarm.get().$promise + .then(function success(data) { + var swarm = new SwarmViewModel(data); + deferred.resolve(swarm); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Swarm details', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/services/docker/taskService.js b/app/services/docker/taskService.js index 44dab1922..13babf0aa 100644 --- a/app/services/docker/taskService.js +++ b/app/services/docker/taskService.js @@ -3,23 +3,6 @@ angular.module('portainer.services') 'use strict'; var service = {}; - service.tasks = function() { - var deferred = $q.defer(); - - Task.query().$promise - .then(function success(data) { - var tasks = data.map(function (item) { - return new TaskViewModel(item); - }); - deferred.resolve(tasks); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve tasks', err: err }); - }); - - return deferred.promise; - }; - service.task = function(id) { var deferred = $q.defer(); @@ -35,10 +18,10 @@ angular.module('portainer.services') return deferred.promise; }; - service.serviceTasks = function(serviceName) { + service.tasks = function(filters) { var deferred = $q.defer(); - Task.query({ filters: { service: [serviceName] } }).$promise + Task.query({ filters: filters ? filters : {} }).$promise .then(function success(data) { var tasks = data.map(function (item) { return new TaskViewModel(item); @@ -46,7 +29,7 @@ angular.module('portainer.services') deferred.resolve(tasks); }) .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve tasks associated to the service', err: err }); + deferred.reject({ msg: 'Unable to retrieve tasks', err: err }); }); return deferred.promise; diff --git a/app/services/fileUpload.js b/app/services/fileUpload.js index 8b256ca00..0f3f46643 100644 --- a/app/services/fileUpload.js +++ b/app/services/fileUpload.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('FileUploadService', ['$q', 'Upload', function FileUploadFactory($q, Upload) { +.factory('FileUploadService', ['$q', 'Upload', 'EndpointProvider', function FileUploadFactory($q, Upload, EndpointProvider) { 'use strict'; var service = {}; @@ -8,6 +8,11 @@ angular.module('portainer.services') return Upload.upload({ url: url, data: { file: file }}); } + service.createStack = function(stackName, swarmId, file) { + var endpointID = EndpointProvider.endpointID(); + return Upload.upload({ url: 'api/endpoints/' + endpointID + '/stacks?method=file', data: { file: file, Name: stackName, SwarmID: swarmId } }); + }; + service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) { var queue = []; diff --git a/app/services/notifications.js b/app/services/notifications.js index 3679cba18..d124f2deb 100644 --- a/app/services/notifications.js +++ b/app/services/notifications.js @@ -7,6 +7,10 @@ angular.module('portainer.services') toastr.success($sanitize(text), $sanitize(title)); }; + service.warning = function(title, text) { + toastr.warning($sanitize(text), $sanitize(title), {timeOut: 6000}); + }; + service.error = function(title, e, fallbackText) { var msg = fallbackText; if (e.data && e.data.message) { @@ -17,13 +21,14 @@ angular.module('portainer.services') msg = e.err.data.message; } else if (e.data && e.data.length > 0 && e.data[0].message) { msg = e.data[0].message; - } else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) { - msg = e.err.data[0].message; - } else if (e.msg) { - msg = e.msg; + } else if (e.err && e.err.data && e.err.data.err) { + msg = e.err.data.err; } else if (e.data && e.data.err) { msg = e.data.err; + } else if (e.msg) { + msg = e.msg; } + if (msg !== 'Invalid JWT token') { toastr.error($sanitize(msg), $sanitize(title), {timeOut: 6000}); } diff --git a/bower.json b/bower.json index 536f8bc04..ed30cca55 100644 --- a/bower.json +++ b/bower.json @@ -50,7 +50,9 @@ "xterm.js": "~2.8.1", "chart.js": "~2.6.0", "angularjs-slider": "^6.4.0", - "angular-ui-router": "~1.0.6" + "angular-ui-router": "~1.0.6", + "codemirror": "~5.30.0", + "js-yaml": "~3.10.0" }, "resolutions": { "angular": "1.5.11" diff --git a/build.sh b/build.sh index 3219679e4..77271abe9 100755 --- a/build.sh +++ b/build.sh @@ -41,9 +41,8 @@ else VERSION="$1" if [ `echo "$@" | cut -c1-4` == 'echo' ]; then bash -c "$@"; - else - build_all 'linux-amd64 linux-386 linux-arm linux-arm64 linux-ppc64le darwin-amd64 windows-amd64' + else + build_all 'linux-amd64 linux-arm linux-arm64 linux-ppc64le darwin-amd64 windows-amd64' exit 0 fi fi - diff --git a/build/download_docker_binary.sh b/build/download_docker_binary.sh new file mode 100755 index 000000000..cc9b94b00 --- /dev/null +++ b/build/download_docker_binary.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +PLATFORM=$1 +ARCH=$2 +DOCKER_VERSION=$3 + +DOWNLOAD_FOLDER=".tmp/download" + +rm -rf "${DOWNLOAD_FOLDER}" +mkdir -pv "${DOWNLOAD_FOLDER}" + +if [ "${PLATFORM}" == 'win' ]; then + wget -O "${DOWNLOAD_FOLDER}/docker-binaries.zip" "https://download.docker.com/${PLATFORM}/static/stable/${ARCH}/docker-${DOCKER_VERSION}.zip" + unzip "${DOWNLOAD_FOLDER}/docker-binaries.zip" -d "${DOWNLOAD_FOLDER}" + mv "${DOWNLOAD_FOLDER}/docker/docker.exe" dist/ +else + wget -O "${DOWNLOAD_FOLDER}/docker-binaries.tgz" "https://download.docker.com/${PLATFORM}/static/stable/${ARCH}/docker-${DOCKER_VERSION}.tgz" + tar -xf "${DOWNLOAD_FOLDER}/docker-binaries.tgz" -C "${DOWNLOAD_FOLDER}" + mv "${DOWNLOAD_FOLDER}/docker/docker" dist/ +fi + +exit 0 diff --git a/build/windows/microsoftservercore/Dockerfile b/build/windows/microsoftservercore/Dockerfile deleted file mode 100644 index dfcd8b15a..000000000 --- a/build/windows/microsoftservercore/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM microsoft/windowsservercore - -COPY dist / - -VOLUME C:\\data - -WORKDIR / - -EXPOSE 9000 - -ENTRYPOINT ["/portainer.exe"] diff --git a/gruntfile.js b/gruntfile.js index ba27bb6c9..d4ff5fede 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -34,6 +34,7 @@ module.exports = function (grunt) { 'config:dev', 'clean:app', 'shell:buildBinary:linux:amd64', + 'shell:downloadDockerBinary:linux:amd64', 'vendor:regular', 'html2js', 'useminPrepare:dev', @@ -44,7 +45,7 @@ module.exports = function (grunt) { 'after-copy' ]); grunt.task.registerTask('release', 'release::', function(p, a) { - grunt.task.run(['config:prod', 'clean:all', 'shell:buildBinary:'+p+':'+a, 'before-copy', 'copy:assets', 'after-copy' ]); + grunt.task.run(['config:prod', 'clean:all', 'shell:buildBinary:'+p+':'+a, 'shell:downloadDockerBinary:'+p+':'+a, 'before-copy', 'copy:assets', 'after-copy' ]); }); grunt.registerTask('lint', ['eslint']); grunt.registerTask('run-dev', ['build', 'shell:run', 'watch:build']); @@ -53,6 +54,7 @@ module.exports = function (grunt) { // Project configuration. grunt.initConfig({ distdir: 'dist', + shippedDockerVersion: '17.09.0-ce', pkg: grunt.file.readJSON('package.json'), config: { dev: { options: { variables: { 'environment': 'development' }}}, @@ -67,7 +69,7 @@ module.exports = function (grunt) { }, clean: { all: ['<%= distdir %>/*'], - app: ['<%= distdir %>/*', '!<%= distdir %>/portainer*'], + app: ['<%= distdir %>/*', '!<%= distdir %>/portainer*', '!<%= distdir %>/docker*'], tmpl: ['<%= distdir %>/templates'], tmp: ['<%= distdir %>/js/*', '!<%= distdir %>/js/app.*.js', '<%= distdir %>/css/*', '!<%= distdir %>/css/app.*.css'] }, @@ -170,19 +172,33 @@ module.exports = function (grunt) { shell: { buildBinary: { command: function (p, a) { - var binfile = 'dist/portainer-'+p+'-'+a; - if (grunt.file.isFile( ( p === 'windows' ) ? binfile+'.exe' : binfile )) { - return 'echo \'BinaryExists\''; - } else { - return 'build/build_in_container.sh ' + p + ' ' + a; - } - } + var binfile = 'dist/portainer-'+p+'-'+a; + if (grunt.file.isFile( ( p === 'windows' ) ? binfile+'.exe' : binfile )) { + return 'echo "Portainer binary exists"'; + } else { + return 'build/build_in_container.sh ' + p + ' ' + a; + } + } }, run: { command: [ 'docker rm -f portainer', 'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer-linux-amd64 --no-analytics -a /app' ].join(';') + }, + downloadDockerBinary: { + command: function(p, a) { + if (p === 'windows') p = 'win'; + if (p === 'darwin') p = 'mac'; + if (a === 'amd64') a = 'x86_64'; + if (a === 'arm') a = 'armhf'; + if (a === 'arm64') a = 'aarch64'; + if (grunt.file.isFile( ( p === 'win' ) ? 'dist/docker.exe' : 'dist/docker' )) { + return 'echo "Docker binary exists"'; + } else { + return 'build/download_docker_binary.sh ' + p + ' ' + a + ' <%= shippedDockerVersion %>'; + } + } } }, replace: { diff --git a/vendor.yml b/vendor.yml index d71e768bf..c052c018d 100644 --- a/vendor.yml +++ b/vendor.yml @@ -12,6 +12,11 @@ js: - bower_components/toastr/toastr.js - bower_components/xterm.js/dist/xterm.js - bower_components/xterm.js/dist/addons/fit/fit.js + - bower_components/js-yaml/dist/js-yaml.js + - bower_components/codemirror/lib/codemirror.js + - bower_components/codemirror/mode/yaml/yaml.js + - bower_components/codemirror/addon/lint/lint.js + - bower_components/codemirror/addon/lint/yaml-lint.js minified: - bower_components/jquery/dist/jquery.min.js - bower_components/bootstrap/dist/js/bootstrap.min.js @@ -25,6 +30,11 @@ js: - bower_components/toastr/toastr.min.js - bower_components/xterm.js/dist/xterm.js - bower_components/xterm.js/dist/addons/fit/fit.js + - bower_components/js-yaml/dist/js-yaml.min.js + - bower_components/codemirror/lib/codemirror.js + - bower_components/codemirror/mode/yaml/yaml.js + - bower_components/codemirror/addon/lint/lint.js + - bower_components/codemirror/addon/lint/yaml-lint.js css: regular: - bower_components/bootstrap/dist/css/bootstrap.css @@ -35,6 +45,8 @@ css: - bower_components/toastr/toastr.css - bower_components/xterm.js/dist/xterm.css - bower_components/angularjs-slider/dist/rzslider.css + - bower_components/codemirror/lib/codemirror.css + - bower_components/codemirror/addon/lint/lint.css minified: - bower_components/bootstrap/dist/css/bootstrap.min.css - bower_components/rdash-ui/dist/css/rdash.min.css @@ -44,6 +56,8 @@ css: - bower_components/toastr/toastr.min.css - bower_components/xterm.js/dist/xterm.css - bower_components/angularjs-slider/dist/rzslider.min.css + - bower_components/codemirror/lib/codemirror.css + - bower_components/codemirror/addon/lint/lint.css angular: regular: - bower_components/angular/angular.js From e11098672891aefa1938b3ff3fb8340229f03963 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 15 Oct 2017 19:27:23 +0200 Subject: [PATCH 20/20] chore(version): bump version number --- api/portainer.go | 2 +- api/swagger.yaml | 4 ++-- bower.json | 2 +- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/portainer.go b/api/portainer.go index b1cfac79a..e1bdf9455 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -386,7 +386,7 @@ type ( const ( // APIVersion is the version number of the Portainer API. - APIVersion = "1.14.3" + APIVersion = "1.15.0" // DBVersion is the version number of the Portainer database. DBVersion = 6 // DefaultTemplatesURL represents the default URL for the templates definitions. diff --git a/api/swagger.yaml b/api/swagger.yaml index 3c220c83b..490e43226 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -56,7 +56,7 @@ info: **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). - version: "1.14.3" + version: "1.15.0" title: "Portainer API" contact: email: "info@portainer.io" @@ -1869,7 +1869,7 @@ definitions: description: "Is analytics enabled" Version: type: "string" - example: "1.14.3" + example: "1.15.0" description: "Portainer API version" PublicSettingsInspectResponse: type: "object" diff --git a/bower.json b/bower.json index ed30cca55..bf9bd43fa 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "portainer", - "version": "1.14.3", + "version": "1.15.0", "homepage": "https://github.com/portainer/portainer", "authors": [ "Anthony Lapenna " diff --git a/package.json b/package.json index 0c6288053..0f48ef054 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.14.3", + "version": "1.15.0", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git"