diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js
index 6daf5d4e7..c50a9183e 100644
--- a/app/docker/helpers/containerHelper.js
+++ b/app/docker/helpers/containerHelper.js
@@ -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;
},
]);
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
index c2bc89cb6..b0e7b5ff4 100644
--- a/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js
@@ -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() {
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form.html b/app/portainer/components/stack-duplication-form/stack-duplication-form.html
index 5e7cf743f..cf2e0a293 100644
--- a/app/portainer/components/stack-duplication-form/stack-duplication-form.html
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form.html
@@ -33,6 +33,9 @@
Duplicate
Duplication in progress...
+
{{ $ctrl.yamlError }}
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form.js b/app/portainer/components/stack-duplication-form/stack-duplication-form.js
index 36e45996f..c9460611d 100644
--- a/app/portainer/components/stack-duplication-form/stack-duplication-form.js
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form.js
@@ -7,5 +7,6 @@ angular.module('portainer.app').component('stackDuplicationForm', {
endpoints: '<',
groups: '<',
currentEndpointId: '<',
+ yamlError: '<',
},
});
diff --git a/app/portainer/helpers/genericHelper.js b/app/portainer/helpers/genericHelper.js
new file mode 100644
index 000000000..47f115890
--- /dev/null
+++ b/app/portainer/helpers/genericHelper.js
@@ -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;
diff --git a/app/portainer/helpers/stackHelper.js b/app/portainer/helpers/stackHelper.js
index 77d43949b..bfaf24b9f 100644
--- a/app/portainer/helpers/stackHelper.js
+++ b/app/portainer/helpers/stackHelper.js
@@ -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;
},
]);
diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js
index 7d226fccb..119f8d2a7 100644
--- a/app/portainer/views/stacks/create/createStackController.js
+++ b/app/portainer/views/stacks/create/createStackController.js
@@ -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();
diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html
index 10f57d321..6719edfec 100644
--- a/app/portainer/views/stacks/create/createstack.html
+++ b/app/portainer/views/stacks/create/createstack.html
@@ -90,6 +90,9 @@
You can get more information about Compose file format in the official documentation.
+ {{ state.editorYamlValidationError }}