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:
parent
fbcf67bc1e
commit
328abfd74e
11 changed files with 114 additions and 53 deletions
5
api/exec/common.go
Normal file
5
api/exec/common.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package exec
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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]+$';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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: '',
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue