fix(docker/stack): prevent stack duplication if name already used (#319)

* fix(docker/stack): prevent stack duplication if name already used

* fix(docker/stack): fix parenthesis error

* fix(docker/stack): fix condition for deploy stack button

* fix(docker/stack): add missing helper

* fix(stacks): show containers only for standalone

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
This commit is contained in:
Alice Groux
2021-06-08 04:45:35 +02:00
committed by GitHub
parent 55e52ee79a
commit a67331f1d4
10 changed files with 106 additions and 5 deletions
+4
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;
},
]);
@@ -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: '<',
},
});
+15
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;
+24
View File
@@ -1,4 +1,6 @@
import _ from 'lodash-es';
import YAML from 'yaml';
import GenericHelper from '@/portainer/helpers/genericHelper';
import { ExternalStackViewModel } from '@/portainer/models/stack';
@@ -22,6 +24,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,13 +8,17 @@ angular
.controller('CreateStackController', function (
$scope,
$state,
$async,
StackService,
Authentication,
Notifications,
FormValidator,
ResourceControlService,
FormHelper,
StackHelper,
ContainerHelper,
CustomTemplateService,
ContainerService,
endpoint
) {
$scope.formValues = {
@@ -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) {
@@ -185,6 +211,13 @@ angular
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve the ComposeSyntaxMaxVersion');
}
try {
const containers = await ContainerService.containers();
$scope.containerNames = ContainerHelper.getContainerNames(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,8 +17,10 @@ angular.module('portainer.app').controller('StackController', [
'EndpointService',
'GroupService',
'ModalService',
'StackHelper',
'endpoint',
'Authentication',
'ContainerHelper',
function (
$async,
$q,
@@ -38,14 +40,17 @@ angular.module('portainer.app').controller('StackController', [
EndpointService,
GroupService,
ModalService,
StackHelper,
endpoint,
Authentication,
ContainerHelper
) {
$scope.state = {
actionInProgress: false,
migrationInProgress: false,
externalStack: false,
showEditorTab: false,
yamlError: false,
};
$scope.formValues = {
@@ -191,6 +196,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;
@@ -247,11 +253,13 @@ 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.containerNames = ContainerHelper.getContainerNames(data.containers);
let resourcesPromise = Promise.resolve({});
if (stack.Status === 1) {
@@ -272,6 +280,7 @@ 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');