Merge branch 'release/1.20.1'

pull/2634/merge 1.20.1
Anthony Lapenna 2019-01-31 13:15:23 +13:00
commit 048c74a0dc
51 changed files with 403 additions and 231 deletions

View File

@ -6,9 +6,8 @@
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/) [![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") [![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) [![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) [![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Slack](https://portainer.io/slack/badge.svg)](https://portainer.io/slack/)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![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) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
@ -20,8 +19,6 @@
## Demo ## Demo
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**). You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
Please note that the public demo cluster is **reset every 15min**. Please note that the public demo cluster is **reset every 15min**.

View File

@ -139,6 +139,7 @@ func (store *Store) MigrateData() error {
DatabaseVersion: version, DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService, EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService, ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService, SettingsService: store.SettingsService,
StackService: store.StackService, StackService: store.StackService,

View File

@ -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
}

View File

@ -5,6 +5,7 @@ import (
"github.com/portainer/portainer" "github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/endpoint" "github.com/portainer/portainer/bolt/endpoint"
"github.com/portainer/portainer/bolt/endpointgroup" "github.com/portainer/portainer/bolt/endpointgroup"
"github.com/portainer/portainer/bolt/extension"
"github.com/portainer/portainer/bolt/resourcecontrol" "github.com/portainer/portainer/bolt/resourcecontrol"
"github.com/portainer/portainer/bolt/settings" "github.com/portainer/portainer/bolt/settings"
"github.com/portainer/portainer/bolt/stack" "github.com/portainer/portainer/bolt/stack"
@ -20,6 +21,7 @@ type (
db *bolt.DB db *bolt.DB
endpointGroupService *endpointgroup.Service endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service endpointService *endpoint.Service
extensionService *extension.Service
resourceControlService *resourcecontrol.Service resourceControlService *resourcecontrol.Service
settingsService *settings.Service settingsService *settings.Service
stackService *stack.Service stackService *stack.Service
@ -35,6 +37,7 @@ type (
DatabaseVersion int DatabaseVersion int
EndpointGroupService *endpointgroup.Service EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service EndpointService *endpoint.Service
ExtensionService *extension.Service
ResourceControlService *resourcecontrol.Service ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service SettingsService *settings.Service
StackService *stack.Service StackService *stack.Service
@ -52,6 +55,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
currentDBVersion: parameters.DatabaseVersion, currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService, endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService, endpointService: parameters.EndpointService,
extensionService: parameters.ExtensionService,
resourceControlService: parameters.ResourceControlService, resourceControlService: parameters.ResourceControlService,
settingsService: parameters.SettingsService, settingsService: parameters.SettingsService,
templateService: parameters.TemplateService, 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) return m.versionService.StoreDBVersion(portainer.DBVersion)
} }

View File

@ -486,7 +486,10 @@ func initExtensionManager(fileService portainer.FileService, extensionService po
for _, extension := range extensions { for _, extension := range extensions {
err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey) err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey)
if err != nil { 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)
} }
} }

View File

@ -113,6 +113,7 @@ func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension,
LicenseKey: licenseKey, LicenseKey: licenseKey,
Company: licenseDetails[0], Company: licenseDetails[0],
Expiration: licenseDetails[1], Expiration: licenseDetails[1],
Valid: true,
} }
extension.Version = licenseDetails[2] extension.Version = licenseDetails[2]

View File

@ -42,7 +42,7 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
} }
for _, existingExtension := range extensions { 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} return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
} }
} }

View File

@ -42,6 +42,7 @@ func associateExtensionData(definition *portainer.Extension, extensions []portai
definition.Enabled = extension.Enabled definition.Enabled = extension.Enabled
definition.License.Company = extension.License.Company definition.License.Company = extension.License.Company
definition.License.Expiration = extension.License.Expiration definition.License.Expiration = extension.License.Expiration
definition.License.Valid = extension.License.Valid
definitionVersion := semver.New(definition.Version) definitionVersion := semver.New(definition.Version)
extensionVersion := semver.New(extension.Version) extensionVersion := semver.New(extension.Version)

View File

@ -66,7 +66,12 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
} }
if payload.LDAPSettings != nil { if payload.LDAPSettings != nil {
ldapPassword := settings.LDAPSettings.Password
if payload.LDAPSettings.Password != "" {
ldapPassword = payload.LDAPSettings.Password
}
settings.LDAPSettings = *payload.LDAPSettings settings.LDAPSettings = *payload.LDAPSettings
settings.LDAPSettings.Password = ldapPassword
} }
if payload.AllowBindMountsForRegularUsers != nil { if payload.AllowBindMountsForRegularUsers != nil {

View File

@ -503,6 +503,7 @@ type (
LicenseKey string `json:"LicenseKey,omitempty"` LicenseKey string `json:"LicenseKey,omitempty"`
Company string `json:"Company,omitempty"` Company string `json:"Company,omitempty"`
Expiration string `json:"Expiration,omitempty"` Expiration string `json:"Expiration,omitempty"`
Valid bool `json:"Valid,omitempty"`
} }
// CLIService represents a service for managing CLI // CLIService represents a service for managing CLI
@ -778,9 +779,9 @@ type (
const ( const (
// APIVersion is the version number of the Portainer API // 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 is the version number of the Portainer database
DBVersion = 16 DBVersion = 17
// AssetsServerURL represents the URL of the Portainer asset server // AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved

View File

@ -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). **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" title: "Portainer API"
contact: contact:
email: "info@portainer.io" email: "info@portainer.io"
@ -525,7 +525,7 @@ paths:
**Access policy**: administrator **Access policy**: administrator
operationId: "EndpointJob" operationId: "EndpointJob"
consumes: consumes:
- "application/json" - "multipart/form-data"
produces: produces:
- "application/json" - "application/json"
security: security:
@ -1434,7 +1434,7 @@ paths:
**Access policy**: restricted **Access policy**: restricted
operationId: "StackCreate" operationId: "StackCreate"
consumes: consumes:
- "application/json" - "multipart/form-data"
produces: produces:
- "application/json" - "application/json"
security: security:
@ -2733,7 +2733,7 @@ paths:
- "application/json" - "application/json"
security: security:
- jwt: [] - jwt: []
parameters: parameters: []
responses: responses:
200: 200:
description: "Success" description: "Success"
@ -3018,7 +3018,7 @@ definitions:
description: "Is analytics enabled" description: "Is analytics enabled"
Version: Version:
type: "string" type: "string"
example: "1.20.0" example: "1.20.1"
description: "Portainer API version" description: "Portainer API version"
PublicSettingsInspectResponse: PublicSettingsInspectResponse:
type: "object" type: "object"
@ -3146,7 +3146,7 @@ definitions:
$ref: "#/definitions/LDAPGroupSearchSettings" $ref: "#/definitions/LDAPGroupSearchSettings"
AutoCreateUsers: AutoCreateUsers:
type: "boolean" type: "boolean"
example: "true" example: true
description: "Automatically provision users and assign them to matching LDAP group names" description: "Automatically provision users and assign them to matching LDAP group names"
Settings: Settings:
@ -3606,6 +3606,7 @@ definitions:
- "Authentication" - "Authentication"
- "Name" - "Name"
- "Password" - "Password"
- "Type"
- "URL" - "URL"
- "Username" - "Username"
properties: properties:
@ -3613,6 +3614,10 @@ definitions:
type: "string" type: "string"
example: "my-registry" example: "my-registry"
description: "Name that will be used to identify this 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: URL:
type: "string" type: "string"
example: "registry.mydomain.tld:2375" example: "registry.mydomain.tld:2375"
@ -4037,7 +4042,7 @@ definitions:
description: "A list of categories associated to the template" description: "A list of categories associated to the template"
items: items:
type: "string" type: "string"
exampe: "database" example: "database"
registry: registry:
type: "string" type: "string"
example: "quay.io" example: "quay.io"
@ -4133,7 +4138,7 @@ definitions:
description: "A list of categories associated to the template" description: "A list of categories associated to the template"
items: items:
type: "string" type: "string"
exampe: "database" example: "database"
registry: registry:
type: "string" type: "string"
example: "quay.io" example: "quay.io"
@ -4233,7 +4238,7 @@ definitions:
description: "A list of categories associated to the template" description: "A list of categories associated to the template"
items: items:
type: "string" type: "string"
exampe: "database" example: "database"
registry: registry:
type: "string" type: "string"
example: "quay.io" example: "quay.io"

View File

@ -1,5 +1,5 @@
{ {
"packageName": "portainer", "packageName": "portainer",
"packageVersion": "1.20.0", "packageVersion": "1.20.1",
"projectName": "portainer" "projectName": "portainer"
} }

View File

@ -30,7 +30,7 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin
}; };
$transitions.onBefore({ to: 'docker.**' }, function() { $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. // to have more controls on which URL should trigger the unauthenticated state.
$rootScope.$on('unauthenticated', function (event, data) { $rootScope.$on('unauthenticated', function (event, data) {
if (!_.includes(data.config.url, '/v2/')) { 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});
} }
}); });
} }

View File

@ -27,6 +27,9 @@ angular.module('portainer')
request: function(config) { request: function(config) {
if (config.url.indexOf('/docker/') > -1) { if (config.url.indexOf('/docker/') > -1) {
config.headers['X-PortainerAgent-Target'] = HttpRequestHelper.portainerAgentTargetHeader(); config.headers['X-PortainerAgent-Target'] = HttpRequestHelper.portainerAgentTargetHeader();
if (HttpRequestHelper.portainerAgentManagerOperation()) {
config.headers['X-PortainerAgent-ManagerOperation'] = '1';
}
} }
return config; return config;
} }

View File

@ -5,7 +5,17 @@ angular.module('portainer.docker', ['portainer.app'])
var docker = { var docker = {
name: 'docker', name: 'docker',
parent: 'root', 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 = { var configs = {

View File

@ -38,12 +38,12 @@
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="(key, value) in $ctrl.dataset | itemsPerPage: $ctrl.state.paginatedItemLimit" ng-class="{active: item.Checked}"> <tr dir-paginate="(key, value) in $ctrl.dataset | itemsPerPage: $ctrl.state.paginatedItemLimit" ng-class="{active: item.Checked}">
<td><a ui-sref="docker.networks.network({ id: value.NetworkID, nodeName: $ctrl.nodeName })">{{ key }}</a></td> <td><a ui-sref="docker.networks.network({ id: key, nodeName: $ctrl.nodeName })">{{ key }}</a></td>
<td>{{ value.IPAddress || '-' }}</td> <td>{{ value.IPAddress || '-' }}</td>
<td>{{ value.Gateway || '-' }}</td> <td>{{ value.Gateway || '-' }}</td>
<td>{{ value.MacAddress || '-' }}</td> <td>{{ value.MacAddress || '-' }}</td>
<td> <td>
<button type="button" class="btn btn-xs btn-danger" ng-disabled="$ctrl.leaveNetworkActionInProgress" button-spinner="$ctrl.leaveNetworkActionInProgress" ng-click="$ctrl.leaveNetworkAction($ctrl.container, value.NetworkID)"> <button type="button" class="btn btn-xs btn-danger" ng-disabled="$ctrl.leaveNetworkActionInProgress" button-spinner="$ctrl.leaveNetworkActionInProgress" ng-click="$ctrl.leaveNetworkAction($ctrl.container, key)">
<span ng-hide="$ctrl.leaveNetworkActionInProgress"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i> Leave network</span> <span ng-hide="$ctrl.leaveNetworkActionInProgress"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i> Leave network</span>
<span ng-show="$ctrl.leaveNetworkActionInProgress">Leaving network...</span> <span ng-show="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
</button> </button>

View File

@ -1,10 +1,9 @@
angular.module('portainer.docker') angular.module('portainer.docker').factory('ContainerHelper', [function ContainerHelperFactory() {
.factory('ContainerHelper', [function ContainerHelperFactory() {
'use strict'; 'use strict';
var helper = {}; var helper = {};
helper.commandStringToArray = function(command) { helper.commandStringToArray = function(command) {
return splitargs(command, undefined, true); return splitargs(command);
}; };
helper.commandArrayToString = function(array) { helper.commandArrayToString = function(array) {

View File

@ -493,6 +493,19 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
$scope.formValues.capabilities.push(new ContainerCapability(cap, false)); $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) { $scope.formValues.capabilities.sort(function(a, b) {
return a.capability < b.capability ? -1 : 1; return a.capability < b.capability ? -1 : 1;
}); });
@ -509,6 +522,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
$scope.fromContainer = fromContainer; $scope.fromContainer = fromContainer;
$scope.config = ContainerHelper.configFromContainer(fromContainer.Model); $scope.config = ContainerHelper.configFromContainer(fromContainer.Model);
loadFromContainerCmd(d); loadFromContainerCmd(d);
loadFromContainerLogging(d);
loadFromContainerPortBindings(d); loadFromContainerPortBindings(d);
loadFromContainerVolumes(d); loadFromContainerVolumes(d);
loadFromContainerNetworkConfig(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() { function initView() {
var nodeName = $transition$.params().nodeName; var nodeName = $transition$.params().nodeName;
$scope.formValues.NodeName = nodeName; $scope.formValues.NodeName = nodeName;
@ -621,9 +646,9 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
function create() { function create() {
var oldContainer = null; var oldContainer = null;
HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName); HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName);
return findCurrentContainer() return findCurrentContainer()
.then(setOldContainer)
.then(confirmCreateContainer) .then(confirmCreateContainer)
.then(startCreationProcess) .then(startCreationProcess)
.catch(notifyOnError) .catch(notifyOnError)
@ -633,6 +658,11 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
$scope.state.actionInProgress = false; $scope.state.actionInProgress = false;
} }
function setOldContainer(container) {
oldContainer = container;
return container;
}
function findCurrentContainer() { function findCurrentContainer() {
return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } }) return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } })
.$promise .$promise
@ -640,8 +670,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
if (!containers.length) { if (!containers.length) {
return; return;
} }
oldContainer = containers[0]; return containers[0];
return oldContainer;
}) })
.catch(notifyOnError); .catch(notifyOnError);
@ -659,12 +688,41 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
} }
$scope.state.actionInProgress = true; $scope.state.actionInProgress = true;
return pullImageIfNeeded() return pullImageIfNeeded()
.then(stopAndRenameContainer(oldContainer)) .then(stopAndRenameContainer)
.then(createNewContainer) .then(createNewContainer)
.then(applyResourceControl) .then(applyResourceControl)
.then(connectToExtraNetworks) .then(connectToExtraNetworks)
.then(removeOldContainer) .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) { function confirmCreateContainer(container) {
@ -695,7 +753,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
} }
} }
function stopAndRenameContainer(oldContainer) { function stopAndRenameContainer() {
if (!oldContainer) { if (!oldContainer) {
return $q.when(); return $q.when();
} }

View File

@ -162,7 +162,7 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input --> <!-- command-input -->
<div class="form-group"> <div class="form-group">
<label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command & logging</label> <label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf"> <input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div> </div>
@ -320,7 +320,7 @@
<span class="input-group-addon">volume</span> <span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.name"> <select class="form-control" ng-model="volume.name">
<option selected disabled hidden value="">Select a volume</option> <option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option> <option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}} - {{ vol.Driver|truncate:30}}</option>
</select> </select>
</div> </div>
<!-- !volume --> <!-- !volume -->

View File

@ -126,6 +126,7 @@ angular.module('portainer.docker')
function createNetwork(context) { function createNetwork(context) {
HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName); HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName);
HttpRequestHelper.setPortainerAgentManagerOperation(context.managerOperation);
$scope.state.actionInProgress = true; $scope.state.actionInProgress = true;
NetworkService.create(context.networkConfiguration) NetworkService.create(context.networkConfiguration)
@ -162,12 +163,17 @@ angular.module('portainer.docker')
var creationContext = { var creationContext = {
nodeName: $scope.formValues.NodeName, nodeName: $scope.formValues.NodeName,
managerOperation: false,
networkConfiguration: networkConfiguration, networkConfiguration: networkConfiguration,
userDetails: userDetails, userDetails: userDetails,
accessControlData: accessControlData, accessControlData: accessControlData,
reload: true 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.config.Driver === 'macvlan') {
if ($scope.formValues.Macvlan.Scope === 'local') { if ($scope.formValues.Macvlan.Scope === 'local') {
modifyNetworkConfigurationForMacvlanConfigOnly(networkConfiguration); modifyNetworkConfigurationForMacvlanConfigOnly(networkConfiguration);
@ -205,4 +211,4 @@ angular.module('portainer.docker')
initView(); initView();
} }
]); ]);

View File

@ -39,13 +39,9 @@
<td>Internal</td> <td>Internal</td>
<td>{{ network.Internal }}</td> <td>{{ network.Internal }}</td>
</tr> </tr>
<tr ng-if="network.IPAM.Config[0].Subnet"> <tr ng-if="network.IPAM.Config.length > 0" ng-repeat="config in network.IPAM.Config">
<td>Subnet</td> <td>Subnet - {{ config.Subnet }}</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td> <td>Gateway - {{ config.Gateway }}</td>
</tr>
<tr ng-if="network.IPAM.Config[0].Gateway">
<td>Gateway</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -315,8 +315,9 @@
<!-- volume --> <!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'"> <div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
<span class="input-group-addon">volume</span> <span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.Source" ng-options="vol.Id|truncate:30 for vol in availableVolumes"> <select class="form-control" ng-model="volume.Source">
<option selected disabled hidden value="">Select a volume</option> <option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Id">{{ vol.Id|truncate:30}} - {{ vol.Driver|truncate:30}}</option>
</select> </select>
</div> </div>
<!-- !volume --> <!-- !volume -->

View File

@ -1,11 +1,13 @@
angular.module('portainer.extensions.registrymanagement') 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'; 'use strict';
return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:action', {}, return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:action', {},
{ {
get: { get: {
method: 'GET', method: 'GET',
params: { id: '@id', action: '_catalog' } params: { id: '@id', action: '_catalog' },
transformResponse: linkGetResponse
}, },
ping: { ping: {
method: 'GET', method: 'GET',

View File

@ -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;
}

View File

@ -11,16 +11,33 @@ function RegistryV2ServiceFactory($q, RegistryCatalog, RegistryTags, RegistryMan
return RegistryCatalog.ping({ id: id }).$promise; 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) { service.repositories = function (id) {
var deferred = $q.defer(); var deferred = $q.defer();
RegistryCatalog.get({ getCatalog(id).then(function success(data) {
id: id
}).$promise
.then(function success(data) {
var promises = []; var promises = [];
for (var i = 0; i < data.repositories.length; i++) { for (var i = 0; i < data.length; i++) {
var repository = data.repositories[i]; var repository = data[i];
promises.push(RegistryTags.get({ promises.push(RegistryTags.get({
id: id, id: id,
repository: repository repository: repository

View File

@ -81,9 +81,17 @@ angular.module('portainer.app')
}); });
return $q.all(promises); return $q.all(promises);
}) })
.then(function success() { .then(function success(data) {
Notifications.success('Success', 'Tags successfully deleted'); 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) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to delete tags'); Notifications.error('Failure', err, 'Unable to delete tags');
@ -127,9 +135,9 @@ angular.module('portainer.app')
}) })
.then(function success(data) { .then(function success(data) {
$scope.registry = data.registry; $scope.registry = data.registry;
$scope.repository.Tags = data.tags; $scope.repository.Tags = [].concat(data.tags || []);
$scope.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]; var tag = data.tags[i];
RegistryV2Service.tag(registryId, repository, tag) RegistryV2Service.tag(registryId, repository, tag)
.then(function success(data) { .then(function success(data) {

View File

@ -48,7 +48,7 @@ angular.module('portainer.app', [])
var authentication = { var authentication = {
name: 'portainer.auth', name: 'portainer.auth',
url: '/auth', url: '/auth?redirect',
params: { params: {
logout: false, logout: false,
error: '' error: ''
@ -87,7 +87,7 @@ angular.module('portainer.app', [])
} }
}; };
var endpointCreation = { var endpointCreation = {
name: 'portainer.endpoints.new', name: 'portainer.endpoints.new',
url: '/new', url: '/new',
views: { views: {
@ -242,7 +242,7 @@ angular.module('portainer.app', [])
} }
}; };
var registryCreation = { var registryCreation = {
name: 'portainer.registries.new', name: 'portainer.registries.new',
url: '/new', url: '/new',
views: { views: {
@ -286,7 +286,7 @@ angular.module('portainer.app', [])
} }
}; };
var scheduleCreation = { var scheduleCreation = {
name: 'portainer.schedules.new', name: 'portainer.schedules.new',
url: '/new', url: '/new',
views: { views: {
@ -327,6 +327,16 @@ angular.module('portainer.app', [])
templateUrl: 'app/portainer/views/stacks/stacks.html', templateUrl: 'app/portainer/views/stacks/stacks.html',
controller: 'StacksController' 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 = { var stackCreation = {
name: 'portainer.newstack', name: 'portainer.stacks.newstack',
url: '/newstack', url: '/newstack',
views: { views: {
'content@': { 'content@': {

View File

@ -11,7 +11,7 @@
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"> ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove <i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button> </button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.newstack"> <button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.stacks.newstack">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack <i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
</button> </button>
</div> </div>

View File

@ -23,11 +23,11 @@
</span> </span>
<span> <span>
<span class="small"> <span class="small" ng-if="$ctrl.model.GroupName">
Group: {{ $ctrl.model.GroupName }} Group: {{ $ctrl.model.GroupName }}
</span> </span>
<button <button
ng-if="$ctrl.isAdmin" ng-if="$ctrl.isAdmin"
class="btn btn-link btn-xs" class="btn btn-link btn-xs"
ng-click="$ctrl.editEndpoint($event)"><i class="fa fa-pencil-alt"></i> ng-click="$ctrl.editEndpoint($event)"><i class="fa fa-pencil-alt"></i>
</button> </button>

View File

@ -21,10 +21,10 @@
</span> </span>
<span> <span>
<span class="label label-primary" ng-if="!$ctrl.model.Enabled && !$ctrl.model.Available">coming soon</span> <span class="label label-primary" ng-if="!$ctrl.model.Enabled && !$ctrl.model.Available">coming soon</span>
<span class="label label-warning" ng-if="!$ctrl.model.Enabled && $ctrl.model.Deal">deal</span> <span class="label label-warning" ng-if="!$ctrl.model.Enabled && $ctrl.model.Deal && !$ctrl.model.License.Expiration">deal</span>
<span class="label label-danger" ng-if="$ctrl.model.Enabled && $ctrl.model.Expired">expired</span> <span class="label label-danger" ng-if="!$ctrl.model.Enabled && $ctrl.model.License.Expiration && !$ctrl.model.License.Valid">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && !$ctrl.model.Expired">enabled</span> <span class="label label-success" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.UpdateAvailable && !$ctrl.model.Expired">update available</span> <span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid && $ctrl.model.UpdateAvailable">update available</span>
</span> </span>
</div> </div>
<!-- !blocklist-item-line1 --> <!-- !blocklist-item-line1 -->

View File

@ -3,7 +3,6 @@ angular.module('portainer.app')
function($state) { function($state) {
var ctrl = this; var ctrl = this;
ctrl.$onInit = $onInit;
ctrl.goToExtensionView = goToExtensionView; ctrl.goToExtensionView = goToExtensionView;
function goToExtensionView() { function goToExtensionView() {
@ -11,10 +10,4 @@ angular.module('portainer.app')
$state.go('portainer.extensions.extension', { id: ctrl.model.Id }); $state.go('portainer.extensions.extension', { id: ctrl.model.Id });
} }
} }
function $onInit() {
if (ctrl.currentDate === ctrl.model.License.Expiration) {
ctrl.model.Expired = true;
}
}
}]); }]);

View File

@ -3,7 +3,7 @@
<div class="blocklist-item-box"> <div class="blocklist-item-box">
<!-- extension-image --> <!-- extension-image -->
<span class="blocklist-item-logo"> <span class="blocklist-item-logo">
<img class="blocklist-item-logo" src="images/support_{{ $ctrl.model.Id }}.png" /> <img class="blocklist-item-logo" ng-src="images/support_{{ $ctrl.model.Id }}.png" />
</span> </span>
<!-- !extension-image --> <!-- !extension-image -->
<!-- extension-details --> <!-- extension-details -->
@ -15,11 +15,6 @@
{{ $ctrl.model.Name }} {{ $ctrl.model.Name }}
</span> </span>
</span> </span>
<span>
<span class="label label-danger" ng-if="$ctrl.model.Enabled && $ctrl.model.Expired">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && !$ctrl.model.Expired">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.UpdateAvailable && !$ctrl.model.Expired">update available</span>
</span>
</div> </div>
<!-- !blocklist-item-line1 --> <!-- !blocklist-item-line1 -->
<!-- blocklist-item-line2 --> <!-- blocklist-item-line2 -->
@ -29,9 +24,6 @@
{{ $ctrl.model.ShortDescription }} {{ $ctrl.model.ShortDescription }}
</span> </span>
</span> </span>
<span ng-if="$ctrl.model.License.Company">
<span class="small text-muted">Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }}</span>
</span>
</div> </div>
<!-- !blocklist-item-line2 --> <!-- !blocklist-item-line2 -->
</span> </span>

View File

@ -1,5 +1,5 @@
angular.module('portainer.app') angular.module('portainer.app')
.factory('EndpointStatusInterceptor', ['$q', '$injector', 'EndpointProvider', function ($q, $injector, EndpointProvider) { .factory('EndpointStatusInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
'use strict'; 'use strict';
var interceptor = {}; var interceptor = {};
@ -18,21 +18,17 @@ angular.module('portainer.app')
} }
function responseInterceptor(response) { function responseInterceptor(response) {
var EndpointService = $injector.get('EndpointService');
var url = response.config.url; var url = response.config.url;
if (response.status === 200 && canBeOffline(url) && EndpointProvider.offlineMode()) { if (response.status === 200 && canBeOffline(url) && EndpointProvider.offlineMode()) {
EndpointProvider.setOfflineMode(false); EndpointProvider.setOfflineMode(false);
EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(false)});
} }
return response || $q.when(response); return response || $q.when(response);
} }
function responseErrorInterceptor(rejection) { function responseErrorInterceptor(rejection) {
var EndpointService = $injector.get('EndpointService');
var url = rejection.config.url; var url = rejection.config.url;
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) { if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
EndpointProvider.setOfflineMode(true); EndpointProvider.setOfflineMode(true);
EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(true)});
} }
return $q.reject(rejection); return $q.reject(rejection);
} }

View File

@ -64,10 +64,6 @@ angular.module('portainer.app')
return endpoint.OfflineMode; return endpoint.OfflineMode;
}; };
service.endpointStatusFromOfflineMode = function(isOffline) {
return isOffline ? 2 : 1;
};
service.setOfflineMode = function(isOffline) { service.setOfflineMode = function(isOffline) {
endpoint.OfflineMode = isOffline; endpoint.OfflineMode = isOffline;
LocalStorage.storeOfflineMode(isOffline); LocalStorage.storeOfflineMode(isOffline);

View File

@ -5,6 +5,7 @@ angular.module('portainer.app')
var service = {}; var service = {};
var headers = {}; var headers = {};
headers.agentTargetQueue = []; headers.agentTargetQueue = [];
headers.agentManagerOperation = false;
service.registryAuthenticationHeader = function() { service.registryAuthenticationHeader = function() {
return headers.registryAuthentication; 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 = []; headers.agentTargetQueue = [];
delete headers.agentTargetLastValue; delete headers.agentTargetLastValue;
headers.agentManagerOperation = false;
}; };
return service; return service;

View File

@ -1,6 +1,6 @@
angular.module('portainer.app') angular.module('portainer.app')
.controller('AuthenticationController', ['$q', '$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', .controller('AuthenticationController', ['$q', '$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', '$stateParams',
function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService) { function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService, $stateParams) {
$scope.logo = StateManager.getState().application.logo; $scope.logo = StateManager.getState().application.logo;
@ -44,7 +44,7 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
if (endpoints.length === 0) { if (endpoints.length === 0) {
$state.go('portainer.init.endpoint'); $state.go('portainer.init.endpoint');
} else { } else {
$state.go('portainer.home'); $state.go($stateParams.redirect ||'portainer.home');
} }
}) })
.catch(function error(err) { .catch(function error(err) {
@ -73,7 +73,7 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
if (endpoints.length === 0 && userDetails.role === 1) { if (endpoints.length === 0 && userDetails.role === 1) {
$state.go('portainer.init.endpoint'); $state.go('portainer.init.endpoint');
} else { } else {
$state.go('portainer.home'); $state.go($stateParams.redirect || 'portainer.home');
} }
}) })
.catch(function error(err) { .catch(function error(err) {

View File

@ -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. 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.
<div style="margin-top: 10px;"> <div style="margin-top: 10px;">
<code> <code>
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
</code> </code>
<span class="btn btn-primary btn-sm space-left" ng-click="copyAgentCommand()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span> <span class="btn btn-primary btn-sm space-left" ng-click="copyAgentCommand()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
<span> <span>

View File

@ -36,7 +36,9 @@
<div class="form-group" style="margin-left: 40px;"> <div class="form-group" style="margin-left: 40px;">
<div style="font-size: 125%; border-bottom: 2px solid #2d3e63; padding-bottom: 5px;"> <div style="font-size: 125%; border-bottom: 2px solid #2d3e63; padding-bottom: 5px;">
{{ extension.Enabled ? 'Enabled' : extension.Price }} <span ng-if="extension.Enabled">Enabled</span>
<span ng-if="!extension.Enabled && extension.License.Expiration && !extension.License.Valid">Expired</span>
<span ng-if="!extension.Enabled && !extension.License.Expiration">{{ extension.Price }}</span>
</div> </div>
<div class="small text-muted col-sm-12" style="margin: 15px 0 15px 0;" ng-if="!extension.Enabled"> <div class="small text-muted col-sm-12" style="margin: 15px 0 15px 0;" ng-if="!extension.Enabled">

View File

@ -1,6 +1,6 @@
angular.module('portainer.app') angular.module('portainer.app')
.controller('TeamsController', ['$q', '$scope', '$state', '$sanitize', 'TeamService', 'UserService', 'ModalService', 'Notifications', 'Authentication', .controller('TeamsController', ['$q', '$scope', '$state', 'TeamService', 'UserService', 'ModalService', 'Notifications', 'Authentication',
function ($q, $scope, $state, $sanitize, TeamService, UserService, ModalService, Notifications, Authentication) { function ($q, $scope, $state, TeamService, UserService, ModalService, Notifications, Authentication) {
$scope.state = { $scope.state = {
actionInProgress: false actionInProgress: false
}; };
@ -22,7 +22,7 @@ function ($q, $scope, $state, $sanitize, TeamService, UserService, ModalService,
}; };
$scope.addTeam = function() { $scope.addTeam = function() {
var teamName = $sanitize($scope.formValues.Name); var teamName = $scope.formValues.Name;
var leaderIds = []; var leaderIds = [];
angular.forEach($scope.formValues.Leaders, function(user) { angular.forEach($scope.formValues.Leaders, function(user) {
leaderIds.push(user.Id); leaderIds.push(user.Id);

View File

@ -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"

View File

@ -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

17
build/build_binary.ps1 Executable file
View File

@ -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"

9
build/build_binary.sh Executable file
View File

@ -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"

24
build/build_binary_devops.ps1 Executable file
View File

@ -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"

15
build/build_binary_devops.sh Executable file
View File

@ -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"

View File

@ -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"

View File

@ -1,5 +1,5 @@
Name: portainer Name: portainer
Version: 1.20.0 Version: 1.20.1
Release: 0 Release: 0
License: Zlib License: Zlib
Summary: A lightweight docker management UI Summary: A lightweight docker management UI

View File

@ -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.run(['config:prod', 'clean:all', 'shell:buildBinary:' + p + ':' + a, 'shell:downloadDockerBinary:' + p + ':' + a, 'before-copy', 'copy:assets', 'after-copy']);
}); });
grunt.task.registerTask('devopsbuild', 'devopsbuild:<platform>:<arch>', 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('lint', ['eslint']);
grunt.registerTask('run-dev', ['build', 'shell:run:' + arch, 'watch:build']); grunt.registerTask('run-dev', ['build', 'shell:run:' + arch, 'watch:build']);
grunt.registerTask('clear', ['clean:app']); grunt.registerTask('clear', ['clean:app']);
@ -261,13 +265,31 @@ gruntfile_cfg.replace = {
function shell_buildBinary(p, a) { function shell_buildBinary(p, a) {
var binfile = 'dist/portainer-' + p + '-' + a; var binfile = 'dist/portainer-' + p + '-' + a;
return [ if (p === 'linux') {
'if [ -f ' + ((p === 'windows') ? binfile + '.exe' : binfile) + ' ]; then', return [
'echo "Portainer binary exists";', 'if [ -f ' + (binfile) + ' ]; then',
'else', 'echo "Portainer binary exists";',
'build/build_in_container.sh ' + p + ' ' + a + ';', 'else',
'fi' 'build/build_binary.sh ' + p + ' ' + a + ';',
].join(' '); '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) { function shell_run(arch) {
@ -283,17 +305,28 @@ function shell_downloadDockerBinary(p, a) {
var ip = ((ps[p] === undefined) ? p : ps[p]); var ip = ((ps[p] === undefined) ? p : ps[p]);
var ia = ((as[a] === undefined) ? a : as[a]); var ia = ((as[a] === undefined) ? a : as[a]);
var binaryVersion = ((p === 'windows' ? '<%= shippedDockerVersionWindows %>' : '<%= shippedDockerVersion %>')); var binaryVersion = ((p === 'windows' ? '<%= shippedDockerVersionWindows %>' : '<%= shippedDockerVersion %>'));
return [ if (p === 'linux') {
'if [ -f ' + ((p === 'windows') ? 'dist/docker.exe' : 'dist/docker') + ' ]; then', return [
'echo "Docker binary exists";', 'if [ -f dist/docker ]; then',
'else', 'echo "Docker binary exists";',
'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'else',
'fi' 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
].join(' '); '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 = { gruntfile_cfg.shell = {
buildBinary: { command: shell_buildBinary }, buildBinary: { command: shell_buildBinary },
buildBinaryOnDevOps: { command: shell_buildBinaryOnDevOps },
run: { command: shell_run }, run: { command: shell_run },
downloadDockerBinary: { command: shell_downloadDockerBinary } downloadDockerBinary: { command: shell_downloadDockerBinary }
}; };

View File

@ -2,7 +2,7 @@
"author": "Portainer.io", "author": "Portainer.io",
"name": "portainer", "name": "portainer",
"homepage": "http://portainer.io", "homepage": "http://portainer.io",
"version": "1.20.0", "version": "1.20.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@github.com:portainer/portainer.git" "url": "git@github.com:portainer/portainer.git"
@ -52,7 +52,7 @@
"angularjs-scroll-glue": "^2.2.0", "angularjs-scroll-glue": "^2.2.0",
"angularjs-slider": "^6.4.0", "angularjs-slider": "^6.4.0",
"bootbox": "^4.4.0", "bootbox": "^4.4.0",
"bootstrap": "~3.3.6", "bootstrap": "^3.4.0",
"chart.js": "~2.6.0", "chart.js": "~2.6.0",
"codemirror": "~5.30.0", "codemirror": "~5.30.0",
"filesize": "~3.3.0", "filesize": "~3.3.0",

View File

@ -874,5 +874,18 @@
"label": "Datadog API key" "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"}]
} }
] ]

View File

@ -139,7 +139,6 @@ angular-mocks@~1.5.0:
angular-moment-picker@^0.10.2: angular-moment-picker@^0.10.2:
version "0.10.2" version "0.10.2"
resolved "https://registry.yarnpkg.com/angular-moment-picker/-/angular-moment-picker-0.10.2.tgz#54c8b3c228b33dffa3b7b3d0773a585323815c33" resolved "https://registry.yarnpkg.com/angular-moment-picker/-/angular-moment-picker-0.10.2.tgz#54c8b3c228b33dffa3b7b3d0773a585323815c33"
integrity sha512-WvmrQM0zEcFqi50yDELaF34Ilrx4PtL7mWLcpTZCJGQDvMlIsxJrB30LxOkoJv8yrrLxD2s6nnR3t1/SqioWWw==
dependencies: dependencies:
angular "^1.3" angular "^1.3"
moment "^2.16.0" moment "^2.16.0"
@ -167,7 +166,6 @@ angular@1.x, angular@~1.5.0:
angular@^1.3: angular@^1.3:
version "1.7.5" version "1.7.5"
resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.5.tgz#d1c1c01c6f5dc835638f3f9aa51012857bdac49e" resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.5.tgz#d1c1c01c6f5dc835638f3f9aa51012857bdac49e"
integrity sha512-760183yxtGzni740IBTieNuWLtPNAoMqvmC0Z62UoU0I3nqk+VJuO3JbQAXOyvo3Oy/ZsdNQwrSTh/B0OQZjNw==
angularjs-scroll-glue@^2.2.0: angularjs-scroll-glue@^2.2.0:
version "2.2.0" version "2.2.0"
@ -407,9 +405,9 @@ bootbox@^4.4.0:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/bootbox/-/bootbox-4.4.0.tgz#ff7f898fb87d4527e547feb64158f88450d1a0c9" resolved "https://registry.yarnpkg.com/bootbox/-/bootbox-4.4.0.tgz#ff7f898fb87d4527e547feb64158f88450d1a0c9"
bootstrap@~3.3.6: bootstrap@^3.4.0:
version "3.3.7" version "3.4.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.4.0.tgz#f8d77540dd3062283d2ae7687e21c1e691961640"
brace-expansion@^1.0.0, brace-expansion@^1.1.7: brace-expansion@^1.0.0, brace-expansion@^1.1.7:
version "1.1.8" version "1.1.8"
@ -2921,7 +2919,6 @@ moment@^2.10.6:
moment@^2.16.0: moment@^2.16.0:
version "2.22.2" version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=
moment@^2.21.0: moment@^2.21.0:
version "2.21.0" version "2.21.0"
@ -4527,7 +4524,6 @@ xtend@~3.0.0:
xterm@^3.8.0: xterm@^3.8.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.8.0.tgz#55d1de518bdc9c9793823f5e4e97d6898972938d" resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.8.0.tgz#55d1de518bdc9c9793823f5e4e97d6898972938d"
integrity sha512-rS3HLryuMWbLsv98+jVVSUXCxmoyXPwqwJNC0ad0VSMdXgl65LefPztQVwfurkaF7kM7ZSgM8eJjnJ9kkdoR1w==
yargs@~3.10.0: yargs@~3.10.0:
version "3.10.0" version "3.10.0"