diff --git a/README.md b/README.md index 3a8035de2..65117ef76 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/) [![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size") [![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable) -[![Build Status](https://semaphoreci.com/api/v1/portainer/portainer-ci/branches/develop/badge.svg)](https://semaphoreci.com/portainer/portainer-ci) +[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop) [![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer) [![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6) diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 0e94ad157..85273c23b 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -139,6 +139,7 @@ func (store *Store) MigrateData() error { DatabaseVersion: version, EndpointGroupService: store.EndpointGroupService, EndpointService: store.EndpointService, + ExtensionService: store.ExtensionService, ResourceControlService: store.ResourceControlService, SettingsService: store.SettingsService, StackService: store.StackService, diff --git a/api/bolt/migrator/migrate_dbversion16.go b/api/bolt/migrator/migrate_dbversion16.go new file mode 100644 index 000000000..4464a87ce --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion16.go @@ -0,0 +1,19 @@ +package migrator + +func (m *Migrator) updateExtensionsToDBVersion17() error { + legacyExtensions, err := m.extensionService.Extensions() + if err != nil { + return err + } + + for _, extension := range legacyExtensions { + extension.License.Valid = true + + err = m.extensionService.Persist(&extension) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 6ec80dcc9..ccee735ff 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -5,6 +5,7 @@ import ( "github.com/portainer/portainer" "github.com/portainer/portainer/bolt/endpoint" "github.com/portainer/portainer/bolt/endpointgroup" + "github.com/portainer/portainer/bolt/extension" "github.com/portainer/portainer/bolt/resourcecontrol" "github.com/portainer/portainer/bolt/settings" "github.com/portainer/portainer/bolt/stack" @@ -20,6 +21,7 @@ type ( db *bolt.DB endpointGroupService *endpointgroup.Service endpointService *endpoint.Service + extensionService *extension.Service resourceControlService *resourcecontrol.Service settingsService *settings.Service stackService *stack.Service @@ -35,6 +37,7 @@ type ( DatabaseVersion int EndpointGroupService *endpointgroup.Service EndpointService *endpoint.Service + ExtensionService *extension.Service ResourceControlService *resourcecontrol.Service SettingsService *settings.Service StackService *stack.Service @@ -52,6 +55,7 @@ func NewMigrator(parameters *Parameters) *Migrator { currentDBVersion: parameters.DatabaseVersion, endpointGroupService: parameters.EndpointGroupService, endpointService: parameters.EndpointService, + extensionService: parameters.ExtensionService, resourceControlService: parameters.ResourceControlService, settingsService: parameters.SettingsService, templateService: parameters.TemplateService, @@ -210,5 +214,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.20.1 + if m.currentDBVersion < 17 { + err := m.updateExtensionsToDBVersion17() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 83f85aa8f..24b5c7097 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -492,7 +492,10 @@ func initExtensionManager(fileService portainer.FileService, extensionService po for _, extension := range extensions { err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey) if err != nil { - return nil, err + log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name) + extension.Enabled = false + extension.License.Valid = false + extensionService.Persist(&extension) } } diff --git a/api/exec/extension.go b/api/exec/extension.go index 20cf2eca8..cb58ecad6 100644 --- a/api/exec/extension.go +++ b/api/exec/extension.go @@ -113,6 +113,7 @@ func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension, LicenseKey: licenseKey, Company: licenseDetails[0], Expiration: licenseDetails[1], + Valid: true, } extension.Version = licenseDetails[2] diff --git a/api/http/handler/extensions/extension_create.go b/api/http/handler/extensions/extension_create.go index b0ce72406..22d146f6e 100644 --- a/api/http/handler/extensions/extension_create.go +++ b/api/http/handler/extensions/extension_create.go @@ -42,7 +42,7 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) } for _, existingExtension := range extensions { - if existingExtension.ID == extensionID { + if existingExtension.ID == extensionID && existingExtension.Enabled { return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled} } } diff --git a/api/http/handler/extensions/extension_list.go b/api/http/handler/extensions/extension_list.go index 392822528..68d26a7e7 100644 --- a/api/http/handler/extensions/extension_list.go +++ b/api/http/handler/extensions/extension_list.go @@ -42,6 +42,7 @@ func associateExtensionData(definition *portainer.Extension, extensions []portai definition.Enabled = extension.Enabled definition.License.Company = extension.License.Company definition.License.Expiration = extension.License.Expiration + definition.License.Valid = extension.License.Valid definitionVersion := semver.New(definition.Version) extensionVersion := semver.New(extension.Version) diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index b4ddb9e96..21b9daa99 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -67,7 +67,12 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * } if payload.LDAPSettings != nil { + ldapPassword := settings.LDAPSettings.Password + if payload.LDAPSettings.Password != "" { + ldapPassword = payload.LDAPSettings.Password + } settings.LDAPSettings = *payload.LDAPSettings + settings.LDAPSettings.Password = ldapPassword } if payload.OAuthSettings != nil { diff --git a/api/portainer.go b/api/portainer.go index 63f886f0f..6f71dc176 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -517,6 +517,7 @@ type ( LicenseKey string `json:"LicenseKey,omitempty"` Company string `json:"Company,omitempty"` Expiration string `json:"Expiration,omitempty"` + Valid bool `json:"Valid,omitempty"` } // CLIService represents a service for managing CLI @@ -799,9 +800,9 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "1.20.0" + APIVersion = "1.20.1" // DBVersion is the version number of the Portainer database - DBVersion = 16 + DBVersion = 17 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved diff --git a/api/swagger.yaml b/api/swagger.yaml index d8a3ad48a..7f5339d2e 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -54,7 +54,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.20.0" + version: "1.20.1" title: "Portainer API" contact: email: "info@portainer.io" @@ -525,7 +525,7 @@ paths: **Access policy**: administrator operationId: "EndpointJob" consumes: - - "application/json" + - "multipart/form-data" produces: - "application/json" security: @@ -1434,7 +1434,7 @@ paths: **Access policy**: restricted operationId: "StackCreate" consumes: - - "application/json" + - "multipart/form-data" produces: - "application/json" security: @@ -2733,7 +2733,7 @@ paths: - "application/json" security: - jwt: [] - parameters: + parameters: [] responses: 200: description: "Success" @@ -3018,7 +3018,7 @@ definitions: description: "Is analytics enabled" Version: type: "string" - example: "1.20.0" + example: "1.20.1" description: "Portainer API version" PublicSettingsInspectResponse: type: "object" @@ -3146,7 +3146,7 @@ definitions: $ref: "#/definitions/LDAPGroupSearchSettings" AutoCreateUsers: type: "boolean" - example: "true" + example: true description: "Automatically provision users and assign them to matching LDAP group names" Settings: @@ -3606,6 +3606,7 @@ definitions: - "Authentication" - "Name" - "Password" + - "Type" - "URL" - "Username" properties: @@ -3613,6 +3614,10 @@ definitions: type: "string" example: "my-registry" description: "Name that will be used to identify this registry" + Type: + type: "integer" + example: 1 + description: "Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)" URL: type: "string" example: "registry.mydomain.tld:2375" @@ -4037,7 +4042,7 @@ definitions: description: "A list of categories associated to the template" items: type: "string" - exampe: "database" + example: "database" registry: type: "string" example: "quay.io" @@ -4133,7 +4138,7 @@ definitions: description: "A list of categories associated to the template" items: type: "string" - exampe: "database" + example: "database" registry: type: "string" example: "quay.io" @@ -4233,7 +4238,7 @@ definitions: description: "A list of categories associated to the template" items: type: "string" - exampe: "database" + example: "database" registry: type: "string" example: "quay.io" diff --git a/api/swagger_config.json b/api/swagger_config.json index beee114ea..cdd9e1115 100644 --- a/api/swagger_config.json +++ b/api/swagger_config.json @@ -1,5 +1,5 @@ { "packageName": "portainer", - "packageVersion": "1.20.0", + "packageVersion": "1.20.1", "projectName": "portainer" } diff --git a/app/app.js b/app/app.js index 021f4d58a..8a05b219b 100644 --- a/app/app.js +++ b/app/app.js @@ -30,7 +30,7 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin }; $transitions.onBefore({ to: 'docker.**' }, function() { - HttpRequestHelper.resetAgentTargetQueue(); + HttpRequestHelper.resetAgentHeaders(); }); }]); @@ -45,7 +45,7 @@ function initAuthentication(authManager, Authentication, $rootScope, $state) { // to have more controls on which URL should trigger the unauthenticated state. $rootScope.$on('unauthenticated', function (event, data) { if (!_.includes(data.config.url, '/v2/')) { - $state.go('portainer.auth', {error: 'Your session has expired'}); + $state.go('portainer.auth', {error: 'Your session has expired', redirect: $state.current.name}); } }); } diff --git a/app/config.js b/app/config.js index cd21e1f08..268b39288 100644 --- a/app/config.js +++ b/app/config.js @@ -27,6 +27,9 @@ angular.module('portainer') request: function(config) { if (config.url.indexOf('/docker/') > -1) { config.headers['X-PortainerAgent-Target'] = HttpRequestHelper.portainerAgentTargetHeader(); + if (HttpRequestHelper.portainerAgentManagerOperation()) { + config.headers['X-PortainerAgent-ManagerOperation'] = '1'; + } } return config; } diff --git a/app/docker/__module.js b/app/docker/__module.js index 9bd99a7cd..d36f0df8c 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -5,7 +5,17 @@ angular.module('portainer.docker', ['portainer.app']) var docker = { name: 'docker', parent: 'root', - abstract: true + abstract: true, + resolve: { + endpointID: ['EndpointProvider', '$state', + function (EndpointProvider, $state) { + var id = EndpointProvider.endpointID(); + if (!id) { + return $state.go('portainer.home'); + } + } + ] + } }; var configs = { diff --git a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html index a79fdc698..bb8a5d4c6 100644 --- a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html +++ b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html @@ -38,12 +38,12 @@ - {{ key }} + {{ key }} {{ value.IPAddress || '-' }} {{ value.Gateway || '-' }} {{ value.MacAddress || '-' }} - diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index f683bf3ff..345f2d1e9 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -1,10 +1,9 @@ -angular.module('portainer.docker') -.factory('ContainerHelper', [function ContainerHelperFactory() { +angular.module('portainer.docker').factory('ContainerHelper', [function ContainerHelperFactory() { 'use strict'; var helper = {}; helper.commandStringToArray = function(command) { - return splitargs(command, undefined, true); + return splitargs(command); }; helper.commandArrayToString = function(array) { diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 44f3af9dd..6dd6acd9f 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -493,6 +493,19 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai $scope.formValues.capabilities.push(new ContainerCapability(cap, false)); }); } + + function hasCapability(item) { + return item.capability === cap.capability; + } + + var capabilities = new ContainerCapabilities(); + for (var i = 0; i < capabilities.length; i++) { + var cap = capabilities[i]; + if (!_.find($scope.formValues.capabilities, hasCapability)) { + $scope.formValues.capabilities.push(cap); + } + } + $scope.formValues.capabilities.sort(function(a, b) { return a.capability < b.capability ? -1 : 1; }); @@ -509,6 +522,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai $scope.fromContainer = fromContainer; $scope.config = ContainerHelper.configFromContainer(fromContainer.Model); loadFromContainerCmd(d); + loadFromContainerLogging(d); loadFromContainerPortBindings(d); loadFromContainerVolumes(d); loadFromContainerNetworkConfig(d); @@ -525,6 +539,17 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai }); } + function loadFromContainerLogging(config) { + var logConfig = config.HostConfig.LogConfig; + $scope.formValues.LogDriverName = logConfig.Type; + $scope.formValues.LogDriverOpts = _.map(logConfig.Config, function (value, name) { + return { + name: name, + value: value + }; + }); + } + function initView() { var nodeName = $transition$.params().nodeName; $scope.formValues.NodeName = nodeName; @@ -621,9 +646,9 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai function create() { var oldContainer = null; - HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName); return findCurrentContainer() + .then(setOldContainer) .then(confirmCreateContainer) .then(startCreationProcess) .catch(notifyOnError) @@ -633,6 +658,11 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai $scope.state.actionInProgress = false; } + function setOldContainer(container) { + oldContainer = container; + return container; + } + function findCurrentContainer() { return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } }) .$promise @@ -640,8 +670,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai if (!containers.length) { return; } - oldContainer = containers[0]; - return oldContainer; + return containers[0]; }) .catch(notifyOnError); @@ -664,7 +693,36 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai .then(applyResourceControl) .then(connectToExtraNetworks) .then(removeOldContainer) - .then(onSuccess); + .then(onSuccess) + .catch(onCreationProcessFail); + } + + function onCreationProcessFail(error) { + var deferred = $q.defer(); + removeNewContainer() + .then(restoreOldContainerName) + .then(function() { + deferred.reject(error); + }) + .catch(function(restoreError) { + deferred.reject(restoreError); + }); + return deferred.promise; + } + + function removeNewContainer() { + return findCurrentContainer().then(function onContainerLoaded(container) { + if (container && (!oldContainer || container.Id !== oldContainer.Id)) { + return ContainerService.remove(container, true); + } + }); + } + + function restoreOldContainerName() { + if (!oldContainer) { + return; + } + return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1)); } function confirmCreateContainer(container) { diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 3f61e44fb..da8521a1c 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -162,7 +162,7 @@
- +
@@ -320,7 +320,7 @@ volume
diff --git a/app/docker/views/networks/create/createNetworkController.js b/app/docker/views/networks/create/createNetworkController.js index a2893f353..eb92079f3 100644 --- a/app/docker/views/networks/create/createNetworkController.js +++ b/app/docker/views/networks/create/createNetworkController.js @@ -126,6 +126,7 @@ angular.module('portainer.docker') function createNetwork(context) { HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName); + HttpRequestHelper.setPortainerAgentManagerOperation(context.managerOperation); $scope.state.actionInProgress = true; NetworkService.create(context.networkConfiguration) @@ -162,12 +163,17 @@ angular.module('portainer.docker') var creationContext = { nodeName: $scope.formValues.NodeName, + managerOperation: false, networkConfiguration: networkConfiguration, userDetails: userDetails, accessControlData: accessControlData, reload: true }; + if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && $scope.config.Driver === 'overlay') { + creationContext.managerOperation = true; + } + if ($scope.config.Driver === 'macvlan') { if ($scope.formValues.Macvlan.Scope === 'local') { modifyNetworkConfigurationForMacvlanConfigOnly(networkConfiguration); @@ -205,4 +211,4 @@ angular.module('portainer.docker') initView(); } - ]); \ No newline at end of file + ]); diff --git a/app/docker/views/networks/edit/network.html b/app/docker/views/networks/edit/network.html index f22562163..0bad80b2e 100644 --- a/app/docker/views/networks/edit/network.html +++ b/app/docker/views/networks/edit/network.html @@ -39,13 +39,9 @@ Internal {{ network.Internal }} - - Subnet - {{ network.IPAM.Config[0].Subnet }} - - - Gateway - {{ network.IPAM.Config[0].Gateway }} + + Subnet - {{ config.Subnet }} + Gateway - {{ config.Gateway }} diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index 2003ff97f..ceca97a55 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -315,8 +315,9 @@
volume - +
diff --git a/app/extensions/registry-management/rest/catalog.js b/app/extensions/registry-management/rest/catalog.js index 1d6f24be5..728a18dcd 100644 --- a/app/extensions/registry-management/rest/catalog.js +++ b/app/extensions/registry-management/rest/catalog.js @@ -1,11 +1,13 @@ angular.module('portainer.extensions.registrymanagement') -.factory('RegistryCatalog', ['$resource', 'API_ENDPOINT_REGISTRIES', function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) { +.factory('RegistryCatalog', ['$resource', 'API_ENDPOINT_REGISTRIES', +function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) { 'use strict'; return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:action', {}, { get: { method: 'GET', - params: { id: '@id', action: '_catalog' } + params: { id: '@id', action: '_catalog' }, + transformResponse: linkGetResponse }, ping: { method: 'GET', diff --git a/app/extensions/registry-management/rest/transform/linkGetResponse.js b/app/extensions/registry-management/rest/transform/linkGetResponse.js new file mode 100644 index 000000000..5307701ca --- /dev/null +++ b/app/extensions/registry-management/rest/transform/linkGetResponse.js @@ -0,0 +1,13 @@ +function linkGetResponse(data, headers) { + var response = angular.fromJson(data); + var link = headers('link'); + if (link) { + var queryString = link.substring(link.indexOf('?') + 1).split('>;')[0]; + var queries = queryString.split('&'); + for (var i = 0; i < queries.length; i++) { + var kv = queries[i].split('='); + response[kv[0]] = kv[1]; + } + } + return response; +} \ No newline at end of file diff --git a/app/extensions/registry-management/services/registryAPIService.js b/app/extensions/registry-management/services/registryAPIService.js index b766ca568..18089bff9 100644 --- a/app/extensions/registry-management/services/registryAPIService.js +++ b/app/extensions/registry-management/services/registryAPIService.js @@ -11,16 +11,33 @@ function RegistryV2ServiceFactory($q, RegistryCatalog, RegistryTags, RegistryMan return RegistryCatalog.ping({ id: id }).$promise; }; + function getCatalog(id) { + var deferred = $q.defer(); + var repositories = []; + + _getCatalogPage({id: id}, deferred, repositories); + + return deferred.promise; + } + + function _getCatalogPage(params, deferred, repositories) { + RegistryCatalog.get(params).$promise.then(function(data) { + repositories = _.concat(repositories, data.repositories); + if (data.last && data.n) { + _getCatalogPage({id: params.id, n: data.n, last: data.last}, deferred, repositories); + } else { + deferred.resolve(repositories); + } + }); + } + service.repositories = function (id) { var deferred = $q.defer(); - RegistryCatalog.get({ - id: id - }).$promise - .then(function success(data) { + getCatalog(id).then(function success(data) { var promises = []; - for (var i = 0; i < data.repositories.length; i++) { - var repository = data.repositories[i]; + for (var i = 0; i < data.length; i++) { + var repository = data[i]; promises.push(RegistryTags.get({ id: id, repository: repository diff --git a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js index de4521cb1..04c7b0e46 100644 --- a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js +++ b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js @@ -81,9 +81,17 @@ angular.module('portainer.app') }); return $q.all(promises); }) - .then(function success() { + .then(function success(data) { Notifications.success('Success', 'Tags successfully deleted'); - $state.reload(); + if (data.length === 0) { + $state.go('portainer.registries.registry.repositories', { + id: $scope.registryId + }, { + reload: true + }); + } else { + $state.reload(); + } }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to delete tags'); @@ -127,9 +135,9 @@ angular.module('portainer.app') }) .then(function success(data) { $scope.registry = data.registry; - $scope.repository.Tags = data.tags; + $scope.repository.Tags = [].concat(data.tags || []); $scope.tags = []; - for (var i = 0; i < data.tags.length; i++) { + for (var i = 0; i < $scope.repository.Tags.length; i++) { var tag = data.tags[i]; RegistryV2Service.tag(registryId, repository, tag) .then(function success(data) { diff --git a/app/portainer/__module.js b/app/portainer/__module.js index b79553a52..129c6b4b2 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -48,7 +48,7 @@ angular.module('portainer.app', []) var authentication = { name: 'portainer.auth', - url: '/auth', + url: '/auth?redirect', params: { logout: false, error: '' @@ -87,7 +87,7 @@ angular.module('portainer.app', []) } }; - var endpointCreation = { + var endpointCreation = { name: 'portainer.endpoints.new', url: '/new', views: { @@ -242,7 +242,7 @@ angular.module('portainer.app', []) } }; - var registryCreation = { + var registryCreation = { name: 'portainer.registries.new', url: '/new', views: { @@ -286,7 +286,7 @@ angular.module('portainer.app', []) } }; - var scheduleCreation = { + var scheduleCreation = { name: 'portainer.schedules.new', url: '/new', views: { @@ -327,6 +327,16 @@ angular.module('portainer.app', []) templateUrl: 'app/portainer/views/stacks/stacks.html', controller: 'StacksController' } + }, + resolve: { + endpointID: ['EndpointProvider', '$state', + function (EndpointProvider, $state) { + var id = EndpointProvider.endpointID(); + if (!id) { + return $state.go('portainer.home'); + } + } + ] } }; @@ -342,7 +352,7 @@ angular.module('portainer.app', []) }; var stackCreation = { - name: 'portainer.newstack', + name: 'portainer.stacks.newstack', url: '/newstack', views: { 'content@': { diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index dc18ed8e1..b9d499949 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -11,7 +11,7 @@ ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"> Remove - diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index a2f96a392..a44f84163 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -23,11 +23,11 @@ - + Group: {{ $ctrl.model.GroupName }} diff --git a/app/portainer/components/extension-list/extension-item/extensionItem.html b/app/portainer/components/extension-list/extension-item/extensionItem.html index 95acdb9cb..18a41ccc4 100644 --- a/app/portainer/components/extension-list/extension-item/extensionItem.html +++ b/app/portainer/components/extension-list/extension-item/extensionItem.html @@ -21,10 +21,10 @@ coming soon - deal - expired - enabled - update available + deal + expired + enabled + update available diff --git a/app/portainer/components/extension-list/extension-item/extensionItemController.js b/app/portainer/components/extension-list/extension-item/extensionItemController.js index 810e93990..841aadc5a 100644 --- a/app/portainer/components/extension-list/extension-item/extensionItemController.js +++ b/app/portainer/components/extension-list/extension-item/extensionItemController.js @@ -3,7 +3,6 @@ angular.module('portainer.app') function($state) { var ctrl = this; - ctrl.$onInit = $onInit; ctrl.goToExtensionView = goToExtensionView; function goToExtensionView() { @@ -11,10 +10,4 @@ angular.module('portainer.app') $state.go('portainer.extensions.extension', { id: ctrl.model.Id }); } } - - function $onInit() { - if (ctrl.currentDate === ctrl.model.License.Expiration) { - ctrl.model.Expired = true; - } - } }]); diff --git a/app/portainer/components/product-list/product-item/productItem.html b/app/portainer/components/product-list/product-item/productItem.html index fa72a7092..b4ac46d6b 100644 --- a/app/portainer/components/product-list/product-item/productItem.html +++ b/app/portainer/components/product-list/product-item/productItem.html @@ -3,7 +3,7 @@
@@ -15,11 +15,6 @@ {{ $ctrl.model.Name }} - - expired - enabled - update available -
@@ -29,9 +24,6 @@ {{ $ctrl.model.ShortDescription }}
- - Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }} - diff --git a/app/portainer/interceptors/endpointStatusInterceptor.js b/app/portainer/interceptors/endpointStatusInterceptor.js index 9411f8d62..5acc075f1 100644 --- a/app/portainer/interceptors/endpointStatusInterceptor.js +++ b/app/portainer/interceptors/endpointStatusInterceptor.js @@ -1,5 +1,5 @@ angular.module('portainer.app') - .factory('EndpointStatusInterceptor', ['$q', '$injector', 'EndpointProvider', function ($q, $injector, EndpointProvider) { + .factory('EndpointStatusInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) { 'use strict'; var interceptor = {}; @@ -18,21 +18,17 @@ angular.module('portainer.app') } function responseInterceptor(response) { - var EndpointService = $injector.get('EndpointService'); var url = response.config.url; if (response.status === 200 && canBeOffline(url) && EndpointProvider.offlineMode()) { EndpointProvider.setOfflineMode(false); - EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(false)}); } return response || $q.when(response); } function responseErrorInterceptor(rejection) { - var EndpointService = $injector.get('EndpointService'); var url = rejection.config.url; if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) { EndpointProvider.setOfflineMode(true); - EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(true)}); } return $q.reject(rejection); } diff --git a/app/portainer/services/endpointProvider.js b/app/portainer/services/endpointProvider.js index ebd89497e..72e38d9f6 100644 --- a/app/portainer/services/endpointProvider.js +++ b/app/portainer/services/endpointProvider.js @@ -64,10 +64,6 @@ angular.module('portainer.app') return endpoint.OfflineMode; }; - service.endpointStatusFromOfflineMode = function(isOffline) { - return isOffline ? 2 : 1; - }; - service.setOfflineMode = function(isOffline) { endpoint.OfflineMode = isOffline; LocalStorage.storeOfflineMode(isOffline); diff --git a/app/portainer/services/httpRequestHelper.js b/app/portainer/services/httpRequestHelper.js index 1d5b5dbe8..d63e2fe2f 100644 --- a/app/portainer/services/httpRequestHelper.js +++ b/app/portainer/services/httpRequestHelper.js @@ -5,6 +5,7 @@ angular.module('portainer.app') var service = {}; var headers = {}; headers.agentTargetQueue = []; + headers.agentManagerOperation = false; service.registryAuthenticationHeader = function() { return headers.registryAuthentication; @@ -36,9 +37,18 @@ angular.module('portainer.app') } }; - service.resetAgentTargetQueue = function() { + service.setPortainerAgentManagerOperation = function(set) { + headers.agentManagerOperation = set; + }; + + service.portainerAgentManagerOperation = function() { + return headers.agentManagerOperation; + }; + + service.resetAgentHeaders = function() { headers.agentTargetQueue = []; delete headers.agentTargetLastValue; + headers.agentManagerOperation = false; }; return service; diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index fb8968e48..8f281a170 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -1,126 +1,125 @@ -angular.module('portainer.app') -.controller('AuthenticationController', ['urlHelper','$q', '$scope', '$state', '$stateParams', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', -function (urlHelper, $q, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService) { - $scope.logo = StateManager.getState().application.logo; +angular.module('portainer.app').controller('AuthenticationController', ['$q', '$scope', '$state', '$stateParams', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', 'urlHelper', + function($q, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService, urlHelper) { + $scope.logo = StateManager.getState().application.logo; - $scope.formValues = { - Username: '', - Password: '' - }; + $scope.formValues = { + Username: '', + Password: '' + }; - $scope.state = { - AuthenticationError: '' - }; + $scope.state = { + AuthenticationError: '' + }; - $scope.authenticateUser = function() { - var username = $scope.formValues.Username; - var password = $scope.formValues.Password; + $scope.authenticateUser = function() { + var username = $scope.formValues.Username; + var password = $scope.formValues.Password; - Authentication.login(username, password) - .then(function success() { - checkForEndpoints(); - }) - .catch(function error() { + Authentication.login(username, password) + .then(function success() { + checkForEndpoints(); + }) + .catch(function error() { + SettingsService.publicSettings() + .then(function success(settings) { + if (settings.AuthenticationMethod === 1) { + return Authentication.login($sanitize(username), $sanitize(password)); + } + return $q.reject(); + }) + .then(function success() { + $state.go('portainer.updatePassword'); + }) + .catch(function error() { + $scope.state.AuthenticationError = 'Invalid credentials'; + }); + }); + }; + + function unauthenticatedFlow() { + EndpointService.endpoints() + .then(function success(endpoints) { + if (endpoints.length === 0) { + $state.go('portainer.init.endpoint'); + } else { + $state.go($stateParams.redirect || 'portainer.home'); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + }); + } + + function authenticatedFlow() { + UserService.administratorExists() + .then(function success(exists) { + if (!exists) { + $state.go('portainer.init.admin'); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to verify administrator account existence'); + }); + } + + function checkForEndpoints() { + EndpointService.endpoints() + .then(function success(data) { + var endpoints = data; + var userDetails = Authentication.getUserDetails(); + + if (endpoints.length === 0 && userDetails.role === 1) { + $state.go('portainer.init.endpoint'); + } else { + $state.go($stateParams.redirect || 'portainer.home'); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + }); + } + + function initView() { SettingsService.publicSettings() - .then(function success(settings) { - if (settings.AuthenticationMethod === 1) { - return Authentication.login($sanitize(username), $sanitize(password)); - } - return $q.reject(); - }) - .then(function success() { - $state.go('portainer.updatePassword'); - }) - .catch(function error() { - $scope.state.AuthenticationError = 'Invalid credentials'; - }); - }); - }; + .then(function success(settings) { + $scope.AuthenticationMethod = settings.AuthenticationMethod; + $scope.OAuthLoginURI = settings.OAuthLoginURI; + }); - function unauthenticatedFlow() { - EndpointService.endpoints() - .then(function success(endpoints) { - if (endpoints.length === 0) { - $state.go('portainer.init.endpoint'); - } else { + if ($stateParams.logout || $stateParams.error) { + Authentication.logout(); + $scope.state.AuthenticationError = $stateParams.error; + return; + } + + if (Authentication.isAuthenticated()) { $state.go('portainer.home'); } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoints'); - }); - } - function authenticatedFlow() { - UserService.administratorExists() - .then(function success(exists) { - if (!exists) { - $state.go('portainer.init.admin'); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to verify administrator account existence'); - }); - } - - function checkForEndpoints() { - EndpointService.endpoints() - .then(function success(data) { - var endpoints = data; - var userDetails = Authentication.getUserDetails(); - - if (endpoints.length === 0 && userDetails.role === 1) { - $state.go('portainer.init.endpoint'); + var authenticationEnabled = $scope.applicationState.application.authentication; + if (!authenticationEnabled) { + unauthenticatedFlow(); } else { - $state.go('portainer.home'); + authenticatedFlow(); } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoints'); - }); - } - function initView() { - SettingsService.publicSettings() - .then(function success(settings) { - $scope.AuthenticationMethod = settings.AuthenticationMethod; - $scope.OAuthLoginURI = settings.OAuthLoginURI; - }); - - if ($stateParams.logout || $stateParams.error) { - Authentication.logout(); - $scope.state.AuthenticationError = $stateParams.error; - return; + var code = urlHelper.getParameter('code'); + if (code) { + oAuthLogin(code); + } } - if (Authentication.isAuthenticated()) { - $state.go('portainer.home'); + function oAuthLogin(code) { + return Authentication.OAuthLogin(code) + .then(function success() { + urlHelper.cleanParameters(); + $state.go('portainer.home'); + }) + .catch(function error() { + $scope.state.AuthenticationError = 'Failed to authenticate with OAuth2 Provider'; + }); } - var authenticationEnabled = $scope.applicationState.application.authentication; - if (!authenticationEnabled) { - unauthenticatedFlow(); - } else { - authenticatedFlow(); - } - var code = urlHelper.getParameter('code'); - if (code) { - oAuthLogin(code); - } - } - - function oAuthLogin(code) { - return Authentication.OAuthLogin(code) - .then(function success() { - urlHelper.cleanParameters(); - $state.go('portainer.home'); - }) - .catch(function error() { - $scope.state.AuthenticationError = 'Failed to authenticate with OAuth2 Provider'; - }); - } - - - initView(); -}]); + initView(); + }]); diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index c682bee1b..e060aa4d6 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -20,7 +20,7 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, }; $scope.copyAgentCommand = function() { - clipboard.copyText('curl -L https://portainer.io/download/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); + clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); $('#copyNotification').show(); $('#copyNotification').fadeOut(2000); }; diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 4b7a762bd..3563a99b9 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -67,7 +67,7 @@ Ensure that you have deployed the Portainer agent in your cluster first. You can use execute the following command on any manager node to deploy it.
- curl -L https://portainer.io/download/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent + curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent Copy diff --git a/app/portainer/views/extensions/inspect/extension.html b/app/portainer/views/extensions/inspect/extension.html index 0a83740d9..df59e2966 100644 --- a/app/portainer/views/extensions/inspect/extension.html +++ b/app/portainer/views/extensions/inspect/extension.html @@ -36,7 +36,9 @@
- {{ extension.Enabled ? 'Enabled' : extension.Price }} + Enabled + Expired + {{ extension.Price }}
diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index f25e5b51f..aef6f23d7 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -1,139 +1,139 @@ angular.module('portainer.app') -.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'LegacyExtensionManager', 'ModalService', 'MotdService', 'SystemService', -function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, LegacyExtensionManager, ModalService, MotdService, SystemService) { + .controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'LegacyExtensionManager', 'ModalService', 'MotdService', 'SystemService', + function($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, LegacyExtensionManager, ModalService, MotdService, SystemService) { - $scope.goToEdit = function(id) { - $state.go('portainer.endpoints.endpoint', { id: id }); - }; + $scope.goToEdit = function(id) { + $state.go('portainer.endpoints.endpoint', { id: id }); + }; - $scope.goToDashboard = function (endpoint) { - if (endpoint.Type === 3) { - return switchToAzureEndpoint(endpoint); - } + $scope.goToDashboard = function(endpoint) { + if (endpoint.Type === 3) { + return switchToAzureEndpoint(endpoint); + } - checkEndpointStatus(endpoint) - .then(function sucess() { - return switchToDockerEndpoint(endpoint); - }).catch(function error(err) { - Notifications.error('Failure', err, 'Unable to verify endpoint status'); - }); - }; + checkEndpointStatus(endpoint) + .then(function sucess() { + return switchToDockerEndpoint(endpoint); + }).catch(function error(err) { + Notifications.error('Failure', err, 'Unable to verify endpoint status'); + }); + }; - $scope.dismissImportantInformation = function (hash) { - StateManager.dismissImportantInformation(hash); - }; + $scope.dismissImportantInformation = function(hash) { + StateManager.dismissImportantInformation(hash); + }; - $scope.dismissInformationPanel = function (id) { - StateManager.dismissInformationPanel(id); - }; + $scope.dismissInformationPanel = function(id) { + StateManager.dismissInformationPanel(id); + }; - $scope.triggerSnapshot = function () { - ModalService.confirmEndpointSnapshot(function (result) { - if (!result) { - return; - } - triggerSnapshot(); - }); - }; + $scope.triggerSnapshot = function() { + ModalService.confirmEndpointSnapshot(function(result) { + if (!result) { + return; + } + triggerSnapshot(); + }); + }; - function checkEndpointStatus(endpoint) { - var deferred = $q.defer(); + function checkEndpointStatus(endpoint) { + var deferred = $q.defer(); + + var status = 1; + SystemService.ping(endpoint.Id) + .then(function sucess() { + status = 1; + }).catch(function error() { + status = 2; + }).finally(function() { + if (endpoint.Status === status) { + deferred.resolve(endpoint); + return deferred.promise; + } + + EndpointService.updateEndpoint(endpoint.Id, { Status: status }) + .then(function sucess() { + deferred.resolve(endpoint); + }).catch(function error(err) { + deferred.reject({ msg: 'Unable to update endpoint status', err: err }); + }); + }); - var status = 1; - SystemService.ping(endpoint.Id) - .then(function sucess() { - status = 1; - }).catch(function error() { - status = 2; - }).finally(function () { - if (endpoint.Status === status) { - deferred.resolve(endpoint); return deferred.promise; } - EndpointService.updateEndpoint(endpoint.Id, { Status: status }) - .then(function sucess() { - deferred.resolve(endpoint); - }).catch(function error(err) { - deferred.reject({msg: 'Unable to update endpoint status', err: err}); - }); - }); + function switchToAzureEndpoint(endpoint) { + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); + StateManager.updateEndpointState(endpoint, []) + .then(function success() { + $state.go('azure.dashboard'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint'); + }); + } - return deferred.promise; - } + function switchToDockerEndpoint(endpoint) { + if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) { + Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.'); + return; + } else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) { + Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.'); + return; + } - function switchToAzureEndpoint(endpoint) { - EndpointProvider.setEndpointID(endpoint.Id); - EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); - EndpointProvider.setOfflineModeFromStatus(endpoint.Status); - StateManager.updateEndpointState(endpoint.Name, endpoint.Type, []) - .then(function success() { - $state.go('azure.dashboard'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint'); - }); - } + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); + LegacyExtensionManager.initEndpointExtensions(endpoint) + .then(function success(data) { + var extensions = data; + return StateManager.updateEndpointState(endpoint, extensions); + }) + .then(function success() { + $state.go('docker.dashboard'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint'); + }); + } - function switchToDockerEndpoint(endpoint) { - if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) { - Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.'); - return; - } else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) { - Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.'); - return; - } + function triggerSnapshot() { + EndpointService.snapshotEndpoints() + .then(function success() { + Notifications.success('Success', 'Endpoints updated'); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'An error occured during endpoint snapshot'); + }); + } - EndpointProvider.setEndpointID(endpoint.Id); - EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); - EndpointProvider.setOfflineModeFromStatus(endpoint.Status); - LegacyExtensionManager.initEndpointExtensions(endpoint) - .then(function success(data) { - var extensions = data; - return StateManager.updateEndpointState(endpoint, extensions); - }) - .then(function success() { - $state.go('docker.dashboard'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint'); - }); - } + function initView() { + $scope.isAdmin = Authentication.getUserDetails().role === 1; - function triggerSnapshot() { - EndpointService.snapshotEndpoints() - .then(function success() { - Notifications.success('Success', 'Endpoints updated'); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'An error occured during endpoint snapshot'); - }); - } + MotdService.motd() + .then(function success(data) { + $scope.motd = data; + }); - function initView() { - $scope.isAdmin = Authentication.getUserDetails().role === 1; + $q.all({ + endpoints: EndpointService.endpoints(), + groups: GroupService.groups() + }) + .then(function success(data) { + var endpoints = data.endpoints; + var groups = data.groups; + EndpointHelper.mapGroupNameToEndpoint(endpoints, groups); + $scope.endpoints = endpoints; + EndpointProvider.setEndpoints(endpoints); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); + }); + } - MotdService.motd() - .then(function success(data) { - $scope.motd = data; - }); - - $q.all({ - endpoints: EndpointService.endpoints(), - groups: GroupService.groups() - }) - .then(function success(data) { - var endpoints = data.endpoints; - var groups = data.groups; - EndpointHelper.mapGroupNameToEndpoint(endpoints, groups); - $scope.endpoints = endpoints; - EndpointProvider.setEndpoints(endpoints); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); - }); - } - - initView(); -}]); + initView(); + }]); diff --git a/app/portainer/views/teams/teamsController.js b/app/portainer/views/teams/teamsController.js index 6c01427ac..9b63b234e 100644 --- a/app/portainer/views/teams/teamsController.js +++ b/app/portainer/views/teams/teamsController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('TeamsController', ['$q', '$scope', '$state', '$sanitize', 'TeamService', 'UserService', 'ModalService', 'Notifications', 'Authentication', -function ($q, $scope, $state, $sanitize, TeamService, UserService, ModalService, Notifications, Authentication) { +.controller('TeamsController', ['$q', '$scope', '$state', 'TeamService', 'UserService', 'ModalService', 'Notifications', 'Authentication', +function ($q, $scope, $state, TeamService, UserService, ModalService, Notifications, Authentication) { $scope.state = { actionInProgress: false }; @@ -22,7 +22,7 @@ function ($q, $scope, $state, $sanitize, TeamService, UserService, ModalService, }; $scope.addTeam = function() { - var teamName = $sanitize($scope.formValues.Name); + var teamName = $scope.formValues.Name; var leaderIds = []; angular.forEach($scope.formValues.Leaders, function(user) { leaderIds.push(user.Id); diff --git a/appveyor-ci.yml b/appveyor-ci.yml deleted file mode 100644 index ab1188611..000000000 --- a/appveyor-ci.yml +++ /dev/null @@ -1,44 +0,0 @@ -version: 1.0.{build} -image: - - Visual Studio 2017 - - Ubuntu -environment: - matrix: - - ARCH: amd64 - - ARCH: arm - - ARCH: arm64 - - ARCH: ppc64le - - ARCH: s390x - DOCKER_USER: - secure: JapmC7j5F0mY3j/MVzU+Cw== - DOCKER_PASS: - secure: QGlCLNWzPD0HL8ipkohVic45/yU3bVOdjn0IiV6NnSQ= -matrix: - exclude: - - image: Visual Studio 2017 - ARCH: arm - - image: Visual Studio 2017 - ARCH: arm64 - - image: Visual Studio 2017 - ARCH: ppc64le - - image: Visual Studio 2017 - ARCH: s390x -branches: - except: - - master -stack: - - node 9, go 1.10 -install: - - yarn install - - npm install -g rebase-docker-image -init: - - sh: export IMAGE=linux - - cmd: SET IMAGE=windows - - ps: >- - if (!(Test-Path ~/.docker)) { mkdir ~/.docker }; - Set-Content -Value '{ "experimental": "enabled" }' -Path ~/.docker/config.json -Encoding Ascii -build_script: - - sh: yarn grunt appveyorbuild:$IMAGE:$ARCH - - cmd: yarn grunt appveyorbuild:%IMAGE%:%ARCH% - - sh: sudo bash build/ci-linux.sh $IMAGE $ARCH $DOCKER_USER $DOCKER_PASS $APPVEYOR_REPO_BRANCH $APPVEYOR_PULL_REQUEST_NUMBER - - cmd: powershell -Command "& .\\build\\ci-windows.ps1" diff --git a/appveyor-release.yml b/appveyor-release.yml deleted file mode 100644 index 6293e6c90..000000000 --- a/appveyor-release.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: 1.0.{build} -image: - - Visual Studio 2017 - - Ubuntu -environment: - matrix: - - ARCH: amd64 - - ARCH: arm - - ARCH: arm64 - - ARCH: ppc64le - - ARCH: s390x - DOCKER_USER: - secure: JapmC7j5F0mY3j/MVzU+Cw== - DOCKER_PASS: - secure: QGlCLNWzPD0HL8ipkohVic45/yU3bVOdjn0IiV6NnSQ= - PORTAINER_VERSION: "1.19.2" -matrix: - exclude: - - image: Visual Studio 2017 - ARCH: arm - - image: Visual Studio 2017 - ARCH: arm64 - - image: Visual Studio 2017 - ARCH: ppc64le - - image: Visual Studio 2017 - ARCH: s390x -branches: - only: - - master -stack: node 9, go 1.10 -artifacts: - - path: 'portainer-$(PORTAINER_VERSION)-$(IMAGE)-$(ARCH).tar.gz' - type: file - - path: 'portainer-$(PORTAINER_VERSION)-$(IMAGE)-$(ARCH)-checksum.txt' - type: file -install: - - yarn install - - npm install -g rebase-docker-image -init: - - sh: export IMAGE=linux - - cmd: SET IMAGE=windows - - ps: >- - if (!(Test-Path ~/.docker)) { mkdir ~/.docker } - Set-Content -Value '{ "experimental": "enabled" }' -Path ~/.docker/config.json -Encoding Ascii -build_script: - - sh: yarn grunt appveyorbuild:$IMAGE:$ARCH - - cmd: yarn grunt appveyorbuild:%IMAGE%:%ARCH% - - sh: sudo bash build/release-linux.sh $IMAGE $ARCH $PORTAINER_VERSION $DOCKER_USER $DOCKER_PASS - - cmd: powershell -Command "& .\\build\\release-windows.ps1" -test: off -deploy: - release: Release $(PORTAINER_VERSION) - description: '' - provider: GitHub - auth_token: - secure: BRYVGj94QlFBCMoO8yhSu+AGqKNV1+03LJEFrNUTRzo5erXfUHUIi/rgztnxfSGW - artifact: /portainer-$(PORTAINER_VERSION)-$(IMAGE)-$(ARCH).*/ - draft: true - prerelease: false - on: - branch: master diff --git a/build/build_binary.ps1 b/build/build_binary.ps1 new file mode 100755 index 000000000..f444f776e --- /dev/null +++ b/build/build_binary.ps1 @@ -0,0 +1,17 @@ +param ( + [string]$platform, + [string]$arch +) + +$ErrorActionPreference = "Stop"; + +$binary = "portainer.exe" +$project_path = (Get-ITEM -Path env:APPVEYOR_BUILD_FOLDER).Value + +New-Item -Name dist -Path "$project_path" -ItemType Directory | Out-Null +Set-Location -Path "$project_path\api\cmd\portainer" + +C:\go\bin\go.exe get -t -d -v ./... +C:\go\bin\go.exe build -v + +Move-Item -Path "$($binary)" -Destination "..\..\..\dist" diff --git a/build/build_binary.sh b/build/build_binary.sh new file mode 100755 index 000000000..b9fe6509c --- /dev/null +++ b/build/build_binary.sh @@ -0,0 +1,9 @@ +binary="portainer" +mkdir -p dist + +cd 'api/cmd/portainer' + +go get -t -d -v ./... +GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' + +mv "${binary}" "../../../dist/portainer" \ No newline at end of file diff --git a/build/build_binary_devops.ps1 b/build/build_binary_devops.ps1 new file mode 100755 index 000000000..6e048b0f3 --- /dev/null +++ b/build/build_binary_devops.ps1 @@ -0,0 +1,24 @@ +param ( + [string]$platform, + [string]$arch +) + +$ErrorActionPreference = "Stop"; + +$binary = "portainer.exe" +$project_path = (Get-ITEM -Path env:BUILD_SOURCESDIRECTORY).Value + +Set-Item env:GOPATH "$project_path\api" + +New-Item -Name dist -Path "$project_path" -ItemType Directory | Out-Null +New-Item -Name portainer -Path "$project_path\api\src\github.com\" -ItemType Directory | Out-Null + +Copy-Item -Path "$project_path\api" -Destination "$project_path\api\src\github.com\portainer" -Recurse -Force -ErrorAction:SilentlyContinue +Rename-Item -Path "$project_path\api\src\github.com\portainer\api" -NewName "portainer" -ErrorAction:SilentlyContinue + +Set-Location -Path "$project_path\api\cmd\portainer" + +go.exe get -t -d -v ./... +go.exe build -v + +Move-Item -Path "$project_path\api\cmd\portainer\$($binary)" -Destination "$project_path\dist" diff --git a/build/build_binary_devops.sh b/build/build_binary_devops.sh new file mode 100755 index 000000000..ccabcc6d7 --- /dev/null +++ b/build/build_binary_devops.sh @@ -0,0 +1,15 @@ +export GOPATH="$BUILD_SOURCESDIRECTORY/api" + +binary="portainer" + +mkdir -p dist +mkdir -p api/src/github.com/portainer/ + +cp -R api/ api/src/github.com/portainer/portainer/ + +cd 'api/cmd/portainer' + +go get -t -d -v ./... +GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' + +mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer" diff --git a/build/download_docker_binary.ps1 b/build/download_docker_binary.ps1 new file mode 100644 index 000000000..b5d6b6c13 --- /dev/null +++ b/build/download_docker_binary.ps1 @@ -0,0 +1,13 @@ +param ( + [string]$docker_version +) + +$ErrorActionPreference = "Stop"; + +New-Item -Path "docker-binary" -ItemType Directory | Out-Null + +$download_folder = "docker-binary" + +Invoke-WebRequest -O "$($download_folder)/docker-binaries.zip" "https://download.docker.com/win/static/stable/x86_64/docker-$($docker_version).zip" +Expand-Archive -Path "$($download_folder)/docker-binaries.zip" -DestinationPath "$($download_folder)" +Move-Item -Path "$($download_folder)/docker/docker.exe" -Destination "dist" diff --git a/distribution/portainer.spec b/distribution/portainer.spec index b974ae153..b4800443e 100644 --- a/distribution/portainer.spec +++ b/distribution/portainer.spec @@ -1,5 +1,5 @@ Name: portainer -Version: 1.20.0 +Version: 1.20.1 Release: 0 License: Zlib Summary: A lightweight docker management UI diff --git a/gruntfile.js b/gruntfile.js index 8379d98f3..defcd07bf 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -51,6 +51,10 @@ module.exports = function(grunt) { grunt.task.run(['config:prod', 'clean:all', 'shell:buildBinary:' + p + ':' + a, 'shell:downloadDockerBinary:' + p + ':' + a, 'before-copy', 'copy:assets', 'after-copy']); }); + grunt.task.registerTask('devopsbuild', 'devopsbuild::', function(p, a) { + grunt.task.run(['config:prod', 'clean:all', 'shell:buildBinaryOnDevOps:' + p + ':' + a, 'shell:downloadDockerBinary:' + p + ':' + a, 'before-copy', 'copy:assets', 'after-copy']); + }); + grunt.registerTask('lint', ['eslint']); grunt.registerTask('run-dev', ['build', 'shell:run:' + arch, 'watch:build']); grunt.registerTask('clear', ['clean:app']); @@ -261,13 +265,31 @@ gruntfile_cfg.replace = { function shell_buildBinary(p, a) { var binfile = 'dist/portainer-' + p + '-' + a; - return [ - 'if [ -f ' + ((p === 'windows') ? binfile + '.exe' : binfile) + ' ]; then', - 'echo "Portainer binary exists";', - 'else', - 'build/build_in_container.sh ' + p + ' ' + a + ';', - 'fi' - ].join(' '); + if (p === 'linux') { + return [ + 'if [ -f ' + (binfile) + ' ]; then', + 'echo "Portainer binary exists";', + 'else', + 'build/build_binary.sh ' + p + ' ' + a + ';', + 'fi' + ].join(' '); + } else { + return [ + 'powershell -Command "& {if (Get-Item -Path ' + binfile + '.exe -ErrorAction:SilentlyContinue) {', + 'Write-Host "Portainer binary exists"', + '} else {', + '& ".\\build\\build_binary.ps1" -platform ' + p + ' -arch ' + a + '', + '}}"' + ].join(' '); + } +} + +function shell_buildBinaryOnDevOps(p, a) { + if (p === 'linux') { + return 'build/build_binary_devops.sh ' + p + ' ' + a + ';'; + } else { + return 'powershell -Command ".\\build\\build_binary_devops.ps1 -platform ' + p + ' -arch ' + a + '"'; + } } function shell_run(arch) { @@ -283,17 +305,28 @@ function shell_downloadDockerBinary(p, a) { var ip = ((ps[p] === undefined) ? p : ps[p]); var ia = ((as[a] === undefined) ? a : as[a]); var binaryVersion = ((p === 'windows' ? '<%= shippedDockerVersionWindows %>' : '<%= shippedDockerVersion %>')); - return [ - 'if [ -f ' + ((p === 'windows') ? 'dist/docker.exe' : 'dist/docker') + ' ]; then', - 'echo "Docker binary exists";', - 'else', - 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', - 'fi' - ].join(' '); + if (p === 'linux') { + return [ + 'if [ -f dist/docker ]; then', + 'echo "Docker binary exists";', + 'else', + 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', + 'fi' + ].join(' '); + } else { + return [ + 'powershell -Command "& {if (Get-Item -Path dist/docker.exe -ErrorAction:SilentlyContinue) {', + 'Write-Host "Docker binary exists"', + '} else {', + '& ".\\build\\download_docker_binary.ps1" -docker_version ' + binaryVersion + '', + '}}"' + ].join(' '); + } } gruntfile_cfg.shell = { buildBinary: { command: shell_buildBinary }, + buildBinaryOnDevOps: { command: shell_buildBinaryOnDevOps }, run: { command: shell_run }, downloadDockerBinary: { command: shell_downloadDockerBinary } }; diff --git a/package.json b/package.json index 2fc6e9772..d6277314c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.20.0", + "version": "1.20.1", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git" @@ -52,7 +52,7 @@ "angularjs-scroll-glue": "^2.2.0", "angularjs-slider": "^6.4.0", "bootbox": "^4.4.0", - "bootstrap": "~3.3.6", + "bootstrap": "^3.4.0", "chart.js": "~2.6.0", "codemirror": "~5.30.0", "filesize": "~3.3.0", diff --git a/templates.json b/templates.json index f99369d9f..c2dfe94a3 100644 --- a/templates.json +++ b/templates.json @@ -874,5 +874,18 @@ "label": "Datadog API key" } ] + }, + { + "type": 1, + "title": "Sonatype Nexus3", + "description": "Sonatype Nexus3 registry manager", + "categories": ["docker"], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/sonatype.png", + "image": "sonatype/nexus3:latest", + "ports": [ + "8081/tcp" + ], + "volumes": [{ "container": "/nexus-data"}] } ] diff --git a/yarn.lock b/yarn.lock index 5e094094b..4877533d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -139,7 +139,6 @@ angular-mocks@~1.5.0: angular-moment-picker@^0.10.2: version "0.10.2" resolved "https://registry.yarnpkg.com/angular-moment-picker/-/angular-moment-picker-0.10.2.tgz#54c8b3c228b33dffa3b7b3d0773a585323815c33" - integrity sha512-WvmrQM0zEcFqi50yDELaF34Ilrx4PtL7mWLcpTZCJGQDvMlIsxJrB30LxOkoJv8yrrLxD2s6nnR3t1/SqioWWw== dependencies: angular "^1.3" moment "^2.16.0" @@ -167,7 +166,6 @@ angular@1.x, angular@~1.5.0: angular@^1.3: version "1.7.5" resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.5.tgz#d1c1c01c6f5dc835638f3f9aa51012857bdac49e" - integrity sha512-760183yxtGzni740IBTieNuWLtPNAoMqvmC0Z62UoU0I3nqk+VJuO3JbQAXOyvo3Oy/ZsdNQwrSTh/B0OQZjNw== angularjs-scroll-glue@^2.2.0: version "2.2.0" @@ -407,9 +405,9 @@ bootbox@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/bootbox/-/bootbox-4.4.0.tgz#ff7f898fb87d4527e547feb64158f88450d1a0c9" -bootstrap@~3.3.6: - version "3.3.7" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71" +bootstrap@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.4.0.tgz#f8d77540dd3062283d2ae7687e21c1e691961640" brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.8" @@ -2921,7 +2919,6 @@ moment@^2.10.6: moment@^2.16.0: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" - integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= moment@^2.21.0: version "2.21.0" @@ -4527,7 +4524,6 @@ xtend@~3.0.0: xterm@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.8.0.tgz#55d1de518bdc9c9793823f5e4e97d6898972938d" - integrity sha512-rS3HLryuMWbLsv98+jVVSUXCxmoyXPwqwJNC0ad0VSMdXgl65LefPztQVwfurkaF7kM7ZSgM8eJjnJ9kkdoR1w== yargs@~3.10.0: version "3.10.0"