diff --git a/api/exec/common.go b/api/exec/common.go new file mode 100644 index 000000000..b5a17d770 --- /dev/null +++ b/api/exec/common.go @@ -0,0 +1,5 @@ +package exec + +import "regexp" + +var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+") \ No newline at end of file diff --git a/api/exec/compose_stack.go b/api/exec/compose_stack.go index 28c4d8278..a30314ca6 100644 --- a/api/exec/compose_stack.go +++ b/api/exec/compose_stack.go @@ -6,7 +6,6 @@ import ( "os" "path" "path/filepath" - "regexp" "strings" "github.com/pkg/errors" @@ -81,8 +80,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S // NormalizeStackName returns a new stack name with unsupported characters replaced func (manager *ComposeStackManager) NormalizeStackName(name string) string { - r := regexp.MustCompile("[^a-z0-9]+") - return r.ReplaceAllString(strings.ToLower(name), "") + return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "") } func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) { diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 6256e92e3..978afc049 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "path" - "regexp" "runtime" "strings" @@ -190,8 +189,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma } func (manager *SwarmStackManager) NormalizeStackName(name string) string { - r := regexp.MustCompile("[^a-z0-9]+") - return r.ReplaceAllString(strings.ToLower(name), "") + return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "") } func configureFilePaths(args []string, filePaths []string) []string { diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 7f3615500..685fd64d8 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -51,8 +51,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } if !isUnique { - errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name) - return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} + return stackExistsError(payload.Name) } stackID := handler.DataStore.Stack().GetNextIdentifier() @@ -157,7 +156,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } if !isUnique { - return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), Err: errStackAlreadyExists} + return stackExistsError(payload.Name) } //make sure the webhook ID is unique @@ -286,8 +285,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } if !isUnique { - errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name) - return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)} + return stackExistsError(payload.Name) } stackID := handler.DataStore.Stack().GetNextIdentifier() diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index e898b5502..66f8e230f 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -57,8 +57,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } if !isUnique { - errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name) - return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} + return stackExistsError(payload.Name) } stackID := handler.DataStore.Stack().GetNextIdentifier() @@ -167,7 +166,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } if !isUnique { - return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), Err: errStackAlreadyExists} + return stackExistsError(payload.Name) } //make sure the webhook ID is unique @@ -305,8 +304,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } if !isUnique { - errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name) - return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} + return stackExistsError(payload.Name) } stackID := handler.DataStore.Stack().GetNextIdentifier() diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index 8795e0fe3..4bb72ec8a 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -45,6 +45,12 @@ type Handler struct { StackDeployer stacks.StackDeployer } +func stackExistsError(name string) (*httperror.HandlerError){ + msg := fmt.Sprintf("A stack with the normalized name '%s' already exists", name) + err := errors.New(msg) + return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: msg, Err: err} +} + // NewHandler creates a handler to manage stack operations. func NewHandler(bouncer *security.RequestBouncer) *Handler { h := &Handler{ diff --git a/app/constants.js b/app/constants.js index 97f9ca362..f33165179 100644 --- a/app/constants.js +++ b/app/constants.js @@ -30,3 +30,4 @@ angular .constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']); export const PORTAINER_FADEOUT = 1500; +export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; 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 f5574518a..db1bb366c 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 @@ -1,3 +1,5 @@ +import { STACK_NAME_VALIDATION_REGEX } from '@/constants'; + angular.module('portainer.app').controller('StackDuplicationFormController', [ 'Notifications', function StackDuplicationFormController(Notifications) { @@ -13,6 +15,8 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [ newName: '', }; + ctrl.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX; + ctrl.isFormValidForDuplication = isFormValidForDuplication; ctrl.isFormValidForMigration = isFormValidForMigration; ctrl.duplicateStack = 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 25ce62913..7f6c65331 100644 --- a/app/portainer/components/stack-duplication-form/stack-duplication-form.html +++ b/app/portainer/components/stack-duplication-form/stack-duplication-form.html @@ -2,40 +2,71 @@
Stack duplication / migration
-
- -

- This feature allows you to duplicate or migrate this stack. -

-
-
-
- -
- - - -
{{ $ctrl.yamlError }}
-
-
+ + +
+
+ +

+ This feature allows you to duplicate or migrate this stack. +

+
+
+ +
+ +
+
+
+
+

+ + This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123'). +

+
+
+
+ +
+ +
+ +
+ + +
+ +
+
+ {{ $ctrl.yamlError }} +
+
+
+
+
diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js index beccf278d..72c609f68 100644 --- a/app/portainer/views/stacks/create/createStackController.js +++ b/app/portainer/views/stacks/create/createStackController.js @@ -1,5 +1,6 @@ import angular from 'angular'; import uuidv4 from 'uuid/v4'; +import { STACK_NAME_VALIDATION_REGEX } from '@/constants'; import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy'; import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel'; @@ -28,6 +29,8 @@ angular $scope.onChangeTemplateId = onChangeTemplateId; $scope.buildAnalyticsProperties = buildAnalyticsProperties; + $scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX; + $scope.formValues = { Name: '', StackFileContent: '', diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html index b6d6d1f21..58db1fe3e 100644 --- a/app/portainer/views/stacks/create/createstack.html +++ b/app/portainer/views/stacks/create/createstack.html @@ -12,7 +12,26 @@
- + +
+
+
+
+
+

+ + This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123'). +

+