mirror of https://github.com/portainer/portainer
fix(stack) normalize stack name EE-1701 (#5776)
* fix(stack) normalize stack name EE-1701 * fix(stack) normalize swarm stack name and fix rebase error EE-1701 * fix(stack) add front end stack name validation EE-1701 * fix(stack) make stack name regex as a const EE-1701 * fix(stack) reuse stack name regex for compose and swarm EE-1701 * fix(stack) add name validation for stack duplication form EE-1701 Co-authored-by: Simon Meng <simon.meng@portainer.io>pull/5819/head
parent
fbcf67bc1e
commit
328abfd74e
|
@ -0,0 +1,5 @@
|
|||
package exec
|
||||
|
||||
import "regexp"
|
||||
|
||||
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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]+$';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -2,40 +2,71 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Stack duplication / migration
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="small" style="margin-top: 10px;">
|
||||
<p class="text-muted">
|
||||
This feature allows you to duplicate or migrate this stack.
|
||||
</p>
|
||||
</span>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Stack name (optional for migration)" aria-placeholder="Stack name" ng-model="$ctrl.formValues.newName" />
|
||||
</div>
|
||||
<endpoint-selector ng-if="$ctrl.endpoints && $ctrl.groups" model="$ctrl.formValues.endpoint" endpoints="$ctrl.endpoints" groups="$ctrl.groups"></endpoint-selector>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.migrateStack()"
|
||||
ng-disabled="$ctrl.isMigrationButtonDisabled()"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.migrationInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.migrationInProgress"> <i class="fa fa-long-arrow-alt-right space-right" aria-hidden="true"></i> Migrate </span>
|
||||
<span ng-show="$ctrl.state.migrationInProgress">Migration in progress...</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.duplicateStack()"
|
||||
ng-disabled="!$ctrl.isFormValidForDuplication() || $ctrl.state.duplicationInProgress || $ctrl.state.migrationInProgress"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.duplicationInProgress"
|
||||
>
|
||||
<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 && $ctrl.isEndpointSelected()"
|
||||
><span class="text-danger small">{{ $ctrl.yamlError }}</span></div
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="dupStackForm">
|
||||
<div class="form-group">
|
||||
<span class="small" style="margin-top: 10px;">
|
||||
<p class="text-muted">
|
||||
This feature allows you to duplicate or migrate this stack.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Stack name (optional for migration)"
|
||||
aria-placeholder="Stack name"
|
||||
name="new_stack_name"
|
||||
ng-pattern="$ctrl.STACK_NAME_VALIDATION_REGEX"
|
||||
ng-model="$ctrl.formValues.newName"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group" ng-show="dupStackForm.new_stack_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="dupStackForm.new_stack_name.$error">
|
||||
<p ng-message="pattern">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span>This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<endpoint-selector ng-if="$ctrl.endpoints && $ctrl.groups" model="$ctrl.formValues.endpoint" endpoints="$ctrl.endpoints" groups="$ctrl.groups"></endpoint-selector>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.migrateStack()"
|
||||
ng-disabled="$ctrl.isMigrationButtonDisabled()"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.migrationInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.migrationInProgress"> <i class="fa fa-long-arrow-alt-right space-right" aria-hidden="true"></i> Migrate </span>
|
||||
<span ng-show="$ctrl.state.migrationInProgress">Migration in progress...</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.duplicateStack()"
|
||||
ng-disabled="!$ctrl.isFormValidForDuplication() || $ctrl.state.duplicationInProgress || $ctrl.state.migrationInProgress"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.duplicationInProgress"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div class="form-group">
|
||||
<div ng-if="$ctrl.yamlError && $ctrl.isEndpointSelected()">
|
||||
<span class="text-danger small">{{ $ctrl.yamlError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -12,7 +12,26 @@
|
|||
<div class="form-group">
|
||||
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="stack_name" placeholder="e.g. mystack" auto-focus />
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="formValues.Name"
|
||||
id="stack_name"
|
||||
name="stack_name"
|
||||
placeholder="e.g. mystack"
|
||||
auto-focus
|
||||
ng-pattern="STACK_NAME_VALIDATION_REGEX"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="createStackForm.stack_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="createStackForm.stack_name.$error">
|
||||
<p ng-message="pattern">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span>This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
|
|
Loading…
Reference in New Issue