portainer/app/portainer/views/stacks/edit/stackController.js

472 lines
15 KiB
JavaScript

import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
angular.module('portainer.app').controller('StackController', [
'$async',
'$q',
'$scope',
'$state',
'$window',
'$transition$',
'StackService',
'NodeService',
'ServiceService',
'TaskService',
'ContainerService',
'ServiceHelper',
'TaskHelper',
'Notifications',
'FormHelper',
'EndpointProvider',
'EndpointService',
'GroupService',
'ModalService',
'StackHelper',
'ResourceControlService',
'Authentication',
'ContainerHelper',
function (
$async,
$q,
$scope,
$state,
$window,
$transition$,
StackService,
NodeService,
ServiceService,
TaskService,
ContainerService,
ServiceHelper,
TaskHelper,
Notifications,
FormHelper,
EndpointProvider,
EndpointService,
GroupService,
ModalService,
StackHelper,
ResourceControlService,
Authentication,
ContainerHelper
) {
$scope.state = {
actionInProgress: false,
migrationInProgress: false,
showEditorTab: false,
yamlError: false,
isEditorDirty: false,
};
$scope.formValues = {
Prune: false,
Endpoint: null,
AccessControlData: new AccessControlFormData(),
Env: [],
};
$window.onbeforeunload = () => {
if ($scope.stackFileContent && $scope.state.isEditorDirty) {
return '';
}
};
$scope.$on('$destroy', function() {
$scope.state.isEditorDirty = false;
})
$scope.handleEnvVarChange = handleEnvVarChange;
function handleEnvVarChange(value) {
$scope.formValues.Env = value;
}
$scope.duplicateStack = function duplicateStack(name, endpointId) {
var stack = $scope.stack;
var env = FormHelper.removeInvalidEnvVars($scope.formValues.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('docker.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 (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));
},
});
});
};
$scope.removeStack = function () {
ModalService.confirmDeletion('Do you want to remove the stack? Associated services will be removed as well.', function onConfirm(confirmed) {
if (!confirmed) {
return;
}
deleteStack();
});
};
function migrateStack(name, endpointId) {
var stack = $scope.stack;
var targetEndpointId = endpointId;
var migrateRequest = StackService.migrateSwarmStack;
if (stack.Type === 2) {
migrateRequest = StackService.migrateComposeStack;
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
// 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 originalEndpointId = EndpointProvider.endpointID();
if (stack.EndpointId === 0) {
stack.EndpointId = originalEndpointId;
}
$scope.state.migrationInProgress = true;
return migrateRequest(stack, targetEndpointId, name)
.then(function success() {
Notifications.success('Stack successfully migrated', stack.Name);
$state.go('docker.stacks', {}, { reload: true });
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to migrate stack');
})
.finally(function final() {
$scope.state.migrationInProgress = false;
});
}
function deleteStack() {
var endpointId = +$state.params.endpointId;
var stack = $scope.stack;
StackService.remove(stack, $transition$.params().external, endpointId)
.then(function success() {
Notifications.success('Stack successfully removed', stack.Name);
$state.go('docker.stacks');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name);
});
}
$scope.associateStack = function () {
var endpointId = +$state.params.endpointId;
var stack = $scope.stack;
var accessControlData = $scope.formValues.AccessControlData;
$scope.state.actionInProgress = true;
StackService.associate(stack, endpointId, $scope.orphanedRunning)
.then(function success(data) {
const resourceControl = data.ResourceControl;
const userDetails = Authentication.getUserDetails();
const userId = userDetails.ID;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
})
.then(function success() {
Notifications.success('Stack successfully associated', stack.Name);
$state.go('docker.stacks');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to associate stack ' + stack.Name);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
$scope.deployStack = function () {
var stackFile = $scope.stackFileContent;
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
var prune = $scope.formValues.Prune;
var stack = $scope.stack;
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
// The EndpointID property is not available for these stacks, we can pass
// the current endpoint identifier as a part of the update request. It will be used if
// the EndpointID property is not defined on the stack.
var endpointId = EndpointProvider.endpointID();
if (stack.EndpointId === 0) {
stack.EndpointId = endpointId;
}
$scope.state.actionInProgress = true;
StackService.updateStack(stack, stackFile, env, prune)
.then(function success() {
Notifications.success('Stack successfully deployed');
$scope.state.isEditorDirty = false;
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create stack');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
$scope.editorUpdate = function (cm) {
if ($scope.stackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== cm.getValue().replace(/(\r\n|\n|\r)/gm, '')) {
$scope.state.isEditorDirty = true;
$scope.stackFileContent = cm.getValue();
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
}
};
$scope.stopStack = stopStack;
function stopStack() {
return $async(stopStackAsync);
}
async function stopStackAsync() {
const confirmed = await ModalService.confirmAsync({
title: 'Are you sure?',
message: 'Are you sure you want to stop this stack?',
buttons: { confirm: { label: 'Stop', className: 'btn-danger' } },
});
if (!confirmed) {
return;
}
$scope.state.actionInProgress = true;
try {
await StackService.stop($scope.stack.Id);
$state.reload();
} catch (err) {
Notifications.error('Failure', err, 'Unable to stop stack');
}
$scope.state.actionInProgress = false;
}
$scope.startStack = startStack;
function startStack() {
return $async(startStackAsync);
}
async function startStackAsync() {
$scope.state.actionInProgress = true;
const id = $scope.stack.Id;
try {
await StackService.start(id);
$state.reload();
} catch (err) {
Notifications.error('Failure', err, 'Unable to start stack');
}
$scope.state.actionInProgress = false;
}
function loadStack(id) {
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
EndpointService.endpoints()
.then(function success(data) {
$scope.endpoints = data.value;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
});
$q.all({
stack: StackService.stack(id),
groups: GroupService.groups(),
containers: ContainerService.containers(true),
})
.then(function success(data) {
var stack = data.stack;
$scope.groups = data.groups;
$scope.stack = stack;
$scope.containerNames = ContainerHelper.getContainerNames(data.containers);
$scope.formValues.Env = $scope.stack.Env;
let resourcesPromise = Promise.resolve({});
if (!stack.Status || stack.Status === 1) {
resourcesPromise = stack.Type === 1 ? retrieveSwarmStackResources(stack.Name, agentProxy) : retrieveComposeStackResources(stack.Name);
}
return $q.all({
stackFile: StackService.getStackFile(id),
resources: resourcesPromise,
});
})
.then(function success(data) {
const isSwarm = $scope.stack.Type === 1;
$scope.stackFileContent = data.stackFile;
// workaround for missing status, if stack has resources, set the status to 1 (active), otherwise to 2 (inactive) (https://github.com/portainer/portainer/issues/4422)
if (!$scope.stack.Status) {
$scope.stack.Status = data.resources && ((isSwarm && data.resources.services.length) || data.resources.containers.length) ? 1 : 2;
}
if ($scope.stack.Status === 1) {
if (isSwarm) {
assignSwarmStackResources(data.resources, agentProxy);
} else {
assignComposeStackResources(data.resources);
}
}
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve stack details');
});
}
function retrieveSwarmStackResources(stackName, agentProxy) {
var stackFilter = {
label: ['com.docker.stack.namespace=' + stackName],
};
return $q.all({
services: ServiceService.services(stackFilter),
tasks: TaskService.tasks(stackFilter),
containers: agentProxy ? ContainerService.containers(1) : [],
nodes: NodeService.nodes(),
});
}
function assignSwarmStackResources(resources, agentProxy) {
var services = resources.services;
var tasks = resources.tasks;
if (agentProxy) {
var containers = resources.containers;
for (var j = 0; j < tasks.length; j++) {
var task = tasks[j];
TaskHelper.associateContainerToTask(task, containers);
}
}
for (var i = 0; i < services.length; i++) {
var service = services[i];
ServiceHelper.associateTasksToService(service, tasks);
}
$scope.nodes = resources.nodes;
$scope.tasks = tasks;
$scope.services = services;
}
function retrieveComposeStackResources(stackName) {
var stackFilter = {
label: ['com.docker.compose.project=' + stackName],
};
return $q.all({
containers: ContainerService.containers(1, stackFilter),
});
}
function assignComposeStackResources(resources) {
$scope.containers = resources.containers;
}
function loadExternalStack(name) {
var stackType = $transition$.params().type;
if (!stackType || (stackType !== '1' && stackType !== '2')) {
Notifications.error('Failure', null, 'Invalid type URL parameter.');
return;
}
if (stackType === '1') {
loadExternalSwarmStack(name);
} else {
loadExternalComposeStack(name);
}
}
function loadExternalSwarmStack(name) {
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
retrieveSwarmStackResources(name, agentProxy)
.then(function success(data) {
assignSwarmStackResources(data, agentProxy);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve stack details');
});
}
function loadExternalComposeStack(name) {
retrieveComposeStackResources(name)
.then(function success(data) {
assignComposeStackResources(data);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve stack details');
});
}
this.uiCanExit = async function () {
if ($scope.stackFileContent && $scope.state.isEditorDirty) {
return ModalService.confirmWebEditorDiscard();
}
};
async function initView() {
var stackName = $transition$.params().name;
$scope.stackName = stackName;
$scope.currentEndpointId = EndpointProvider.endpointID();
const regular = $transition$.params().regular == 'true';
$scope.regular = regular;
var external = $transition$.params().external == 'true';
$scope.external = external;
const orphaned = $transition$.params().orphaned == 'true';
$scope.orphaned = orphaned;
const orphanedRunning = $transition$.params().orphanedRunning == 'true';
$scope.orphanedRunning = orphanedRunning;
if (external || (orphaned && orphanedRunning)) {
loadExternalStack(stackName);
}
if (regular || orphaned) {
const stackId = $transition$.params().id;
loadStack(stackId);
}
try {
const endpoint = EndpointProvider.currentEndpoint();
$scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion;
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve the ComposeSyntaxMaxVersion');
}
$scope.stackType = $transition$.params().type;
}
initView();
},
]);