feat(stack): prevent stack duplication if name already used (#4740)
* feat(stack): prevent stack duplication if name already used * refacto(stack): deduplicate functions and rename variables * refacto(stack): add a generic helper for findDeepAll function * fix(templates): remove forgotten conflict markerspull/4484/head
parent
f03cf2a6e4
commit
36fcbb9e18
|
@ -259,6 +259,10 @@ angular.module('portainer.docker').factory('ContainerHelper', [
|
|||
return bindings;
|
||||
};
|
||||
|
||||
helper.getContainerNames = function (containers) {
|
||||
return _.map(_.flatten(_.map(containers, 'Names')), (name) => _.trimStart(name, '/'));
|
||||
};
|
||||
|
||||
return helper;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -24,7 +24,7 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
|||
}
|
||||
|
||||
function isFormValidForDuplication() {
|
||||
return isFormValidForMigration() && ctrl.formValues.newName;
|
||||
return isFormValidForMigration() && ctrl.formValues.newName && !ctrl.yamlError;
|
||||
}
|
||||
|
||||
function duplicateStack() {
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
<span ng-hide="$ctrl.state.duplicationInProgress"> <i class="fa fa-clone space-right" aria-hidden="true"></i> Duplicate </span>
|
||||
<span ng-show="$ctrl.state.duplicationInProgress">Duplication in progress...</span>
|
||||
</button>
|
||||
<div ng-if="$ctrl.yamlError"
|
||||
><span class="text-danger small">{{ $ctrl.yamlError }}</span></div
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,5 +7,6 @@ angular.module('portainer.app').component('stackDuplicationForm', {
|
|||
endpoints: '<',
|
||||
groups: '<',
|
||||
currentEndpointId: '<',
|
||||
yamlError: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
class GenericHelper {
|
||||
static findDeepAll(obj, target, res = []) {
|
||||
if (typeof obj === 'object') {
|
||||
_.forEach(obj, (child, key) => {
|
||||
if (key === target) res.push(child);
|
||||
if (typeof child === 'object') GenericHelper.findDeepAll(child, target, res);
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export default GenericHelper;
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
import YAML from 'yaml';
|
||||
import GenericHelper from '@/portainer/helpers/genericHelper';
|
||||
import { ExternalStackViewModel } from '@/portainer/models/stack';
|
||||
|
||||
angular.module('portainer.app').factory('StackHelper', [
|
||||
|
@ -22,6 +23,28 @@ angular.module('portainer.app').factory('StackHelper', [
|
|||
);
|
||||
}
|
||||
|
||||
helper.validateYAML = function (yaml, containerNames) {
|
||||
let yamlObject;
|
||||
|
||||
try {
|
||||
yamlObject = YAML.parse(yaml);
|
||||
} catch (err) {
|
||||
return 'There is an error in the yaml syntax: ' + err;
|
||||
}
|
||||
|
||||
const names = _.uniq(GenericHelper.findDeepAll(yamlObject, 'container_name'));
|
||||
const duplicateContainers = _.intersection(containerNames, names);
|
||||
|
||||
if (duplicateContainers.length === 0) return;
|
||||
|
||||
return (
|
||||
(duplicateContainers.length === 1 ? 'This container name is' : 'These container names are') +
|
||||
' already used by another container running in this environment: ' +
|
||||
_.join(duplicateContainers, ', ') +
|
||||
'.'
|
||||
);
|
||||
};
|
||||
|
||||
return helper;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -8,14 +8,18 @@ angular
|
|||
.controller('CreateStackController', function (
|
||||
$scope,
|
||||
$state,
|
||||
$async,
|
||||
StackService,
|
||||
Authentication,
|
||||
Notifications,
|
||||
FormValidator,
|
||||
ResourceControlService,
|
||||
FormHelper,
|
||||
EndpointProvider,
|
||||
StackHelper,
|
||||
ContainerHelper,
|
||||
CustomTemplateService,
|
||||
EndpointProvider
|
||||
ContainerService
|
||||
) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
|
@ -36,6 +40,8 @@ angular
|
|||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
StackType: null,
|
||||
editorYamlValidationError: '',
|
||||
uploadYamlValidationError: '',
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function () {
|
||||
|
@ -154,6 +160,26 @@ angular
|
|||
|
||||
$scope.editorUpdate = function (cm) {
|
||||
$scope.formValues.StackFileContent = cm.getValue();
|
||||
$scope.state.editorYamlValidationError = StackHelper.validateYAML($scope.formValues.StackFileContent, $scope.containerNames);
|
||||
};
|
||||
|
||||
async function onFileLoadAsync(event) {
|
||||
$scope.state.uploadYamlValidationError = StackHelper.validateYAML(event.target.result, $scope.containerNames);
|
||||
}
|
||||
|
||||
function onFileLoad(event) {
|
||||
return $async(onFileLoadAsync, event);
|
||||
}
|
||||
|
||||
$scope.uploadFile = function (file) {
|
||||
$scope.formValues.StackFile = file;
|
||||
|
||||
if (file) {
|
||||
const temporaryFileReader = new FileReader();
|
||||
temporaryFileReader.fileName = file.name;
|
||||
temporaryFileReader.onload = onFileLoad;
|
||||
temporaryFileReader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onChangeTemplate = async function onChangeTemplate(template) {
|
||||
|
@ -186,6 +212,13 @@ angular
|
|||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve the ComposeSyntaxMaxVersion');
|
||||
}
|
||||
|
||||
try {
|
||||
$scope.containers = await ContainerService.containers();
|
||||
$scope.containerNames = ContainerHelper.getContainerNames($scope.containers);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve Containers');
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
|
|
|
@ -90,6 +90,9 @@
|
|||
<span class="col-sm-12 text-muted small">
|
||||
You can get more information about Compose file format in the <a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
|
||||
</span>
|
||||
<div class="col-sm-12" ng-if="state.editorYamlValidationError"
|
||||
><span class="text-danger small">{{ state.editorYamlValidationError }}</span></div
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
@ -112,10 +115,13 @@
|
|||
<span class="col-sm-12 text-muted small">
|
||||
You can upload a Compose file from your computer.
|
||||
</span>
|
||||
<div class="col-sm-12" ng-if="state.uploadYamlValidationError"
|
||||
><span class="text-danger small">{{ state.uploadYamlValidationError }}</span></div
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="formValues.StackFile">Select file</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select="uploadFile($file)">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.StackFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.StackFile" aria-hidden="true"></i>
|
||||
|
@ -235,6 +241,11 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Web editor
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" ng-if="state.editorYamlValidationError"
|
||||
><span class="text-danger small">{{ state.editorYamlValidationError }}</span></div
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
|
@ -292,9 +303,9 @@
|
|||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress
|
||||
|| (state.Method === 'editor' && !formValues.StackFileContent)
|
||||
|| (state.Method === 'upload' && !formValues.StackFile)
|
||||
|| (state.Method === 'template' && (!formValues.StackFileContent || !selectedTemplate))
|
||||
|| (state.Method === 'editor' && (!formValues.StackFileContent || state.editorYamlValidationError))
|
||||
|| (state.Method === 'upload' && (!formValues.StackFile || state.uploadYamlValidationError))
|
||||
|| (state.Method === 'template' && (!formValues.StackFileContent || !selectedTemplate || state.editorYamlValidationError))
|
||||
|| (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && (!formValues.RepositoryUsername || !formValues.RepositoryPassword))))
|
||||
|| !formValues.Name"
|
||||
ng-click="deployStack()"
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
current-endpoint-id="currentEndpointId"
|
||||
on-duplicate="duplicateStack(name, endpointId)"
|
||||
on-migrate="migrateStack(name, endpointId)"
|
||||
yaml-error="state.yamlError"
|
||||
>
|
||||
</stack-duplication-form>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ angular.module('portainer.app').controller('StackController', [
|
|||
'EndpointService',
|
||||
'GroupService',
|
||||
'ModalService',
|
||||
'StackHelper',
|
||||
function (
|
||||
$async,
|
||||
$q,
|
||||
|
@ -35,13 +36,16 @@ angular.module('portainer.app').controller('StackController', [
|
|||
EndpointProvider,
|
||||
EndpointService,
|
||||
GroupService,
|
||||
ModalService
|
||||
ModalService,
|
||||
StackHelper,
|
||||
ContainerHelper
|
||||
) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
migrationInProgress: false,
|
||||
externalStack: false,
|
||||
showEditorTab: false,
|
||||
yamlError: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
|
@ -187,6 +191,7 @@ angular.module('portainer.app').controller('StackController', [
|
|||
|
||||
$scope.editorUpdate = function (cm) {
|
||||
$scope.stackFileContent = cm.getValue();
|
||||
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
|
||||
};
|
||||
|
||||
$scope.stopStack = stopStack;
|
||||
|
@ -243,11 +248,14 @@ angular.module('portainer.app').controller('StackController', [
|
|||
$q.all({
|
||||
stack: StackService.stack(id),
|
||||
groups: GroupService.groups(),
|
||||
containers: ContainerService.containers(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var stack = data.stack;
|
||||
$scope.groups = data.groups;
|
||||
$scope.stack = stack;
|
||||
$scope.containers = data.containers;
|
||||
$scope.containerNames = ContainerHelper.getContainerNames($scope.containers);
|
||||
|
||||
let resourcesPromise = Promise.resolve({});
|
||||
if (stack.Status === 1) {
|
||||
|
@ -268,6 +276,8 @@ angular.module('portainer.app').controller('StackController', [
|
|||
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');
|
||||
|
|
Loading…
Reference in New Issue