diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go
index 8a0ec0c69..704394766 100644
--- a/api/http/handler/stacks/stack_migrate.go
+++ b/api/http/handler/stacks/stack_migrate.go
@@ -14,6 +14,7 @@ import (
type stackMigratePayload struct {
EndpointID int
SwarmID string
+ Name string
}
func (payload *stackMigratePayload) Validate(r *http.Request) error {
@@ -89,11 +90,17 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
stack.SwarmID = payload.SwarmID
}
+ oldName := stack.Name
+ if payload.Name != "" {
+ stack.Name = payload.Name
+ }
+
migrationError := handler.migrateStack(r, stack, targetEndpoint)
if migrationError != nil {
return migrationError
}
+ stack.Name = oldName
err = handler.deleteStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
diff --git a/api/swagger.yaml b/api/swagger.yaml
index 1fda7d40f..ba5745801 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -4160,6 +4160,10 @@ definitions:
type: "string"
example: "jpofkc0i9uo9wtx1zesuk649w"
description: "Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated"
+ Name:
+ type: "string"
+ example: "new-stack"
+ description: "If provided will rename the migrated stack"
StackCreateRequest:
type: "object"
required:
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js b/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js
new file mode 100644
index 000000000..4e96696e2
--- /dev/null
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js
@@ -0,0 +1,76 @@
+angular.module('portainer.app').controller('StackDuplicationFormController', [
+ 'Notifications',
+ function StackDuplicationFormController(Notifications) {
+ var ctrl = this;
+
+ ctrl.state = {
+ duplicationInProgress: false,
+ migrationInProgress: false
+ };
+
+ ctrl.formValues = {
+ endpoint: null,
+ newName: ''
+ };
+
+ ctrl.isFormValidForDuplication = isFormValidForDuplication;
+ ctrl.isFormValidForMigration = isFormValidForMigration;
+ ctrl.duplicateStack = duplicateStack;
+ ctrl.migrateStack = migrateStack;
+ ctrl.isMigrationButtonDisabled = isMigrationButtonDisabled;
+
+ function isFormValidForMigration() {
+ return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id;
+ }
+
+ function isFormValidForDuplication() {
+ return isFormValidForMigration() && ctrl.formValues.newName;
+ }
+
+ function duplicateStack() {
+ if (!ctrl.formValues.newName) {
+ Notifications.error(
+ 'Failure',
+ null,
+ 'Stack name is required for duplication'
+ );
+ return;
+ }
+ ctrl.state.duplicationInProgress = true;
+ ctrl.onDuplicate({
+ endpointId: ctrl.formValues.endpoint.Id,
+ name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined
+ })
+ .finally(function() {
+ ctrl.state.duplicationInProgress = false;
+ });
+ }
+
+ function migrateStack() {
+ ctrl.state.migrationInProgress = true;
+ ctrl.onMigrate({
+ endpointId: ctrl.formValues.endpoint.Id,
+ name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined
+ })
+ .finally(function() {
+ ctrl.state.migrationInProgress = false;
+ });
+ }
+
+ function isMigrationButtonDisabled() {
+ return (
+ !ctrl.isFormValidForMigration() ||
+ ctrl.state.duplicationInProgress ||
+ ctrl.state.migrationInProgress ||
+ isTargetEndpointAndCurrentEquals()
+ );
+ }
+
+ function isTargetEndpointAndCurrentEquals() {
+ return (
+ ctrl.formValues.endpoint &&
+ ctrl.formValues.endpoint.Id === ctrl.currentEndpointId
+ );
+ }
+ }
+]);
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form.html b/app/portainer/components/stack-duplication-form/stack-duplication-form.html
new file mode 100644
index 000000000..6e270b7b0
--- /dev/null
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form.html
@@ -0,0 +1,43 @@
+
+
+ Stack duplication / migration
+
+
+
\ No newline at end of file
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form.js b/app/portainer/components/stack-duplication-form/stack-duplication-form.js
new file mode 100644
index 000000000..7f6180c39
--- /dev/null
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form.js
@@ -0,0 +1,12 @@
+angular.module('portainer.app').component('stackDuplicationForm', {
+ templateUrl:
+ 'app/portainer/components/stack-duplication-form/stack-duplication-form.html',
+ controller: 'StackDuplicationFormController',
+ bindings: {
+ onDuplicate: '&',
+ onMigrate: '&',
+ endpoints: '<',
+ groups: '<',
+ currentEndpointId: '<'
+ }
+});
diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js
index dafbc4379..37b7c10a9 100644
--- a/app/portainer/services/api/stackService.js
+++ b/app/portainer/services/api/stackService.js
@@ -4,6 +4,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
'use strict';
var service = {};
+
service.stack = function(id) {
var deferred = $q.defer();
@@ -33,7 +34,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.migrateSwarmStack = function(stack, targetEndpointId) {
+ service.migrateSwarmStack = function(stack, targetEndpointId, newName) {
var deferred = $q.defer();
EndpointProvider.setEndpointID(targetEndpointId);
@@ -45,8 +46,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
deferred.reject({ msg: 'Target endpoint is located in the same Swarm cluster as the current endpoint', err: null });
return;
}
-
- return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id }).$promise;
+ return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id, Name: newName }).$promise;
})
.then(function success() {
deferred.resolve();
@@ -61,12 +61,12 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.migrateComposeStack = function(stack, targetEndpointId) {
+ service.migrateComposeStack = function(stack, targetEndpointId, newName) {
var deferred = $q.defer();
EndpointProvider.setEndpointID(targetEndpointId);
- Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId }).$promise
+ Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, Name: newName }).$promise
.then(function success() {
deferred.resolve();
})
@@ -258,8 +258,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
var deferred = $q.defer();
SwarmService.swarm()
- .then(function success(data) {
- var swarm = data;
+ .then(function success(swarm) {
var payload = {
Name: name,
SwarmID: swarm.Id,
@@ -321,5 +320,10 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
+ service.duplicateStack = function duplicateStack(name, stackFileContent, env, endpointId, type) {
+ var action = type === 1 ? service.createSwarmStackFromFileContent : service.createComposeStackFromFileContent;
+ return action(name, stackFileContent, env, endpointId);
+ };
+
return service;
}]);
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html
index 85defd1e2..7a0c1427e 100644
--- a/app/portainer/views/stacks/edit/stack.html
+++ b/app/portainer/views/stacks/edit/stack.html
@@ -46,31 +46,15 @@
-
-
-
- Stack migration
-
-
-
-
+
+
diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js
index 50a8651b2..55eba59c4 100644
--- a/app/portainer/views/stacks/edit/stackController.js
+++ b/app/portainer/views/stacks/edit/stackController.js
@@ -14,24 +14,47 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
Endpoint: null
};
+ $scope.duplicateStack = function duplicateStack(name, endpointId) {
+ var stack = $scope.stack;
+ var env = FormHelper.removeInvalidEnvVars(stack.Env);
+ EndpointProvider.setEndpointID(endpointId);
+
+ return StackService.duplicateStack(name, $scope.stackFileContent, env, endpointId, stack.Type)
+ .then(onDuplicationSuccess)
+ .catch(notifyOnError);
+
+ function onDuplicationSuccess() {
+ Notifications.success('Stack successfully duplicated');
+ $state.go('portainer.stacks', {}, { reload: true });
+ EndpointProvider.setEndpointID(stack.EndpointId);
+
+ }
+
+ function notifyOnError(err) {
+ Notifications.error('Failure', err, 'Unable to duplicate stack');
+ }
+ };
+
$scope.showEditor = function() {
$scope.state.showEditorTab = true;
};
- $scope.migrateStack = function() {
- ModalService.confirm({
- title: 'Are you sure?',
- message: 'This action will deploy a new instance of this stack on the target endpoint, please note that this does NOT relocate the content of any persistent volumes that may be attached to this stack.',
- buttons: {
- confirm: {
- label: 'Migrate',
- className: 'btn-danger'
+ $scope.migrateStack = function (name, endpointId) {
+ return $q(function (resolve) {
+ ModalService.confirm({
+ title: 'Are you sure?',
+ message: 'This action will deploy a new instance of this stack on the target endpoint, please note that this does NOT relocate the content of any persistent volumes that may be attached to this stack.',
+ buttons: {
+ confirm: {
+ label: 'Migrate',
+ className: 'btn-danger'
+ }
+ },
+ callback: function onConfirm(confirmed) {
+ if (!confirmed) { return resolve(); }
+ return resolve(migrateStack(name, endpointId));
}
- },
- callback: function onConfirm(confirmed) {
- if(!confirmed) { return; }
- migrateStack();
- }
+ });
});
};
@@ -45,9 +68,9 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
);
};
- function migrateStack() {
+ function migrateStack(name, endpointId) {
var stack = $scope.stack;
- var targetEndpointId = $scope.formValues.Endpoint.Id;
+ var targetEndpointId = endpointId;
var migrateRequest = StackService.migrateSwarmStack;
if (stack.Type === 2) {
@@ -58,13 +81,13 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
// The EndpointID property is not available for these stacks, we can pass
// the current endpoint identifier as a part of the migrate request. It will be used if
// the EndpointID property is not defined on the stack.
- var endpointId = EndpointProvider.endpointID();
+ var originalEndpointId = EndpointProvider.endpointID();
if (stack.EndpointId === 0) {
- stack.EndpointId = endpointId;
+ stack.EndpointId = originalEndpointId;
}
$scope.state.migrationInProgress = true;
- migrateRequest(stack, targetEndpointId)
+ return migrateRequest(stack, targetEndpointId, name)
.then(function success() {
Notifications.success('Stack successfully migrated', stack.Name);
$state.go('portainer.stacks', {}, {reload: true});
@@ -134,7 +157,6 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
function loadStack(id) {
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
- var endpointId = EndpointProvider.endpointID();
$q.all({
stack: StackService.stack(id),
@@ -143,9 +165,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
})
.then(function success(data) {
var stack = data.stack;
- $scope.endpoints = data.endpoints.filter(function(endpoint) {
- return endpoint.Id !== endpointId;
- });
+ $scope.endpoints = data.endpoints;
$scope.groups = data.groups;
$scope.stack = stack;
@@ -256,6 +276,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
var stackName = $transition$.params().name;
$scope.stackName = stackName;
var external = $transition$.params().external;
+ $scope.currentEndpointId = EndpointProvider.endpointID();
if (external === 'true') {
$scope.state.externalStack = true;