1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-21 14:29:40 +02:00

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>
This commit is contained in:
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" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/pkg/errors" "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 // NormalizeStackName returns a new stack name with unsupported characters replaced
func (manager *ComposeStackManager) NormalizeStackName(name string) string { func (manager *ComposeStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+") return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
return r.ReplaceAllString(strings.ToLower(name), "")
} }
func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) { func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {

View file

@ -8,7 +8,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"regexp"
"runtime" "runtime"
"strings" "strings"
@ -190,8 +189,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
} }
func (manager *SwarmStackManager) NormalizeStackName(name string) string { func (manager *SwarmStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+") return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
return r.ReplaceAllString(strings.ToLower(name), "")
} }
func configureFilePaths(args []string, filePaths []string) []string { 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} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
} }
if !isUnique { if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name) return stackExistsError(payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
} }
stackID := handler.DataStore.Stack().GetNextIdentifier() 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} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
} }
if !isUnique { 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 //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} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
} }
if !isUnique { if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name) return stackExistsError(payload.Name)
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
} }
stackID := handler.DataStore.Stack().GetNextIdentifier() 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} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
} }
if !isUnique { if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name) return stackExistsError(payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
} }
stackID := handler.DataStore.Stack().GetNextIdentifier() 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} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
} }
if !isUnique { 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 //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} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
} }
if !isUnique { if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name) return stackExistsError(payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
} }
stackID := handler.DataStore.Stack().GetNextIdentifier() stackID := handler.DataStore.Stack().GetNextIdentifier()

View file

@ -45,6 +45,12 @@ type Handler struct {
StackDeployer stacks.StackDeployer 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. // NewHandler creates a handler to manage stack operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler { func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{ h := &Handler{

View file

@ -30,3 +30,4 @@ angular
.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']); .constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']);
export const PORTAINER_FADEOUT = 1500; 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', [ angular.module('portainer.app').controller('StackDuplicationFormController', [
'Notifications', 'Notifications',
function StackDuplicationFormController(Notifications) { function StackDuplicationFormController(Notifications) {
@ -13,6 +15,8 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
newName: '', newName: '',
}; };
ctrl.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
ctrl.isFormValidForDuplication = isFormValidForDuplication; ctrl.isFormValidForDuplication = isFormValidForDuplication;
ctrl.isFormValidForMigration = isFormValidForMigration; ctrl.isFormValidForMigration = isFormValidForMigration;
ctrl.duplicateStack = duplicateStack; ctrl.duplicateStack = duplicateStack;

View file

@ -2,17 +2,43 @@
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Stack duplication / migration Stack duplication / migration
</div> </div>
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="dupStackForm">
<div class="form-group"> <div class="form-group">
<span class="small" style="margin-top: 10px;"> <span class="small" style="margin-top: 10px;">
<p class="text-muted"> <p class="text-muted">
This feature allows you to duplicate or migrate this stack. This feature allows you to duplicate or migrate this stack.
</p> </p>
</span> </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> </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> <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 <button
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
ng-click="$ctrl.migrateStack()" ng-click="$ctrl.migrateStack()"
@ -33,9 +59,14 @@
<span ng-hide="$ctrl.state.duplicationInProgress"> <i class="fa fa-clone space-right" aria-hidden="true"></i> Duplicate </span> <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> <span ng-show="$ctrl.state.duplicationInProgress">Duplication in progress...</span>
</button> </button>
<div ng-if="$ctrl.yamlError && $ctrl.isEndpointSelected()" </div>
><span class="text-danger small">{{ $ctrl.yamlError }}</span></div
> <div class="form-group">
<div ng-if="$ctrl.yamlError && $ctrl.isEndpointSelected()">
<span class="text-danger small">{{ $ctrl.yamlError }}</span>
</div> </div>
</div> </div>
</form>
</rd-widget-body>
</rd-widget>
</div> </div>

View file

@ -1,5 +1,6 @@
import angular from 'angular'; import angular from 'angular';
import uuidv4 from 'uuid/v4'; import uuidv4 from 'uuid/v4';
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy'; import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel'; import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
@ -28,6 +29,8 @@ angular
$scope.onChangeTemplateId = onChangeTemplateId; $scope.onChangeTemplateId = onChangeTemplateId;
$scope.buildAnalyticsProperties = buildAnalyticsProperties; $scope.buildAnalyticsProperties = buildAnalyticsProperties;
$scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
$scope.formValues = { $scope.formValues = {
Name: '', Name: '',
StackFileContent: '', StackFileContent: '',

View file

@ -12,7 +12,26 @@
<div class="form-group"> <div class="form-group">
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label> <label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11"> <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>
</div> </div>
<!-- !name-input --> <!-- !name-input -->