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
cong meng 2021-10-01 16:56:34 +13:00 committed by GitHub
parent fbcf67bc1e
commit 328abfd74e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 53 deletions

5
api/exec/common.go Normal file
View File

@ -0,0 +1,5 @@
package exec
import "regexp"
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]+$';

View File

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

View File

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

View File

@ -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: '',

View File

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