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 markers
pull/4484/head
Maxime Bajeux 2021-03-03 14:54:35 +01:00 committed by GitHub
parent f03cf2a6e4
commit 36fcbb9e18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 109 additions and 8 deletions

View File

@ -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;
},
]);

View File

@ -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() {

View File

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

View File

@ -7,5 +7,6 @@ angular.module('portainer.app').component('stackDuplicationForm', {
endpoints: '<',
groups: '<',
currentEndpointId: '<',
yamlError: '<',
},
});

View File

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

View File

@ -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;
},
]);

View File

@ -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();

View File

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

View File

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

View File

@ -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');