mirror of https://github.com/portainer/portainer
fix(custom-templates): relax custom template validation and enforce stack name validation [EE-7102] (#11937)
Co-authored-by: testa113 <testa113>pull/11972/head
parent
5182220d0a
commit
e7af3296fc
|
@ -142,7 +142,14 @@ export const ngModule = angular
|
|||
),
|
||||
{ stackName: 'setStackName' }
|
||||
),
|
||||
['setStackName', 'stackName', 'stacks', 'inputClassName', 'textTip']
|
||||
[
|
||||
'setStackName',
|
||||
'stackName',
|
||||
'stacks',
|
||||
'inputClassName',
|
||||
'textTip',
|
||||
'error',
|
||||
]
|
||||
)
|
||||
)
|
||||
.component(
|
||||
|
|
|
@ -172,6 +172,7 @@
|
|||
text-tip="'Enter or select a \'stack\' name to group multiple deployments together, or else leave empty to ignore.'"
|
||||
stacks="ctrl.stacks"
|
||||
input-class-name="'col-lg-10 col-sm-9'"
|
||||
error="ctrl.state.stackNameError"
|
||||
></kube-stack-name>
|
||||
<!-- #endregion -->
|
||||
|
||||
|
@ -234,6 +235,7 @@
|
|||
text-tip="'Enter or select a \'stack\' name to group multiple deployments together, or else leave empty to ignore.'"
|
||||
stacks="ctrl.stacks"
|
||||
input-class-name="'col-lg-10 col-sm-9'"
|
||||
error="ctrl.state.stackNameError"
|
||||
></kube-stack-name>
|
||||
<!-- #endregion -->
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import { confirmUpdateAppIngress } from '@/react/kubernetes/applications/CreateV
|
|||
import { confirm, confirmUpdate, confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
import { ModalType } from '@@/modals';
|
||||
import { KUBE_STACK_NAME_VALIDATION_REGEX } from '@/react/kubernetes/DeployView/StackName/constants';
|
||||
|
||||
class KubernetesCreateApplicationController {
|
||||
/* #region CONSTRUCTOR */
|
||||
|
@ -127,6 +128,7 @@ class KubernetesCreateApplicationController {
|
|||
// a validation message will be shown. isExistingCPUReservationUnchanged and isExistingMemoryReservationUnchanged (with available resources being exceeded) is used to decide whether to show the message or not.
|
||||
isExistingCPUReservationUnchanged: false,
|
||||
isExistingMemoryReservationUnchanged: false,
|
||||
stackNameError: '',
|
||||
};
|
||||
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
|
@ -186,9 +188,16 @@ class KubernetesCreateApplicationController {
|
|||
}
|
||||
/* #endregion */
|
||||
|
||||
onChangeStackName(stackName) {
|
||||
onChangeStackName(name) {
|
||||
return this.$async(async () => {
|
||||
this.formValues.StackName = stackName;
|
||||
if (KUBE_STACK_NAME_VALIDATION_REGEX.test(name) || name === '') {
|
||||
this.state.stackNameError = '';
|
||||
} else {
|
||||
this.state.stackNameError =
|
||||
"Stack must consist of alphanumeric characters, '-', '_' or '.', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123').";
|
||||
}
|
||||
|
||||
this.formValues.StackName = name;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -644,7 +653,8 @@ class KubernetesCreateApplicationController {
|
|||
const invalid = !this.isValid();
|
||||
const hasNoChanges = this.isEditAndNoChangesMade();
|
||||
const nonScalable = this.isNonScalable();
|
||||
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable;
|
||||
const stackNameInvalid = this.state.stackNameError !== '';
|
||||
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || stackNameInvalid;
|
||||
}
|
||||
|
||||
isUpdateApplicationViaWebEditorButtonDisabled() {
|
||||
|
|
|
@ -90,7 +90,12 @@
|
|||
<div class="w-fit mb-4">
|
||||
<stack-name-label-insight></stack-name-label-insight>
|
||||
</div>
|
||||
<kube-stack-name stack-name="ctrl.formValues.StackName" set-stack-name="(ctrl.setStackName)" stacks="ctrl.stacks"></kube-stack-name>
|
||||
<kube-stack-name
|
||||
stack-name="ctrl.formValues.StackName"
|
||||
set-stack-name="(ctrl.setStackName)"
|
||||
stacks="ctrl.stacks"
|
||||
error="ctrl.state.stackNameError"
|
||||
></kube-stack-name>
|
||||
</div>
|
||||
<!-- !namespace -->
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/p
|
|||
import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
import { KUBE_STACK_NAME_VALIDATION_REGEX } from '@/react/kubernetes/DeployView/StackName/constants';
|
||||
|
||||
class KubernetesDeployController {
|
||||
/* @ngInject */
|
||||
|
@ -57,6 +58,7 @@ class KubernetesDeployController {
|
|||
templateLoadFailed: false,
|
||||
isEditorReadOnly: false,
|
||||
selectedHelmChart: '',
|
||||
stackNameError: '',
|
||||
};
|
||||
|
||||
this.currentUser = {
|
||||
|
@ -117,7 +119,16 @@ class KubernetesDeployController {
|
|||
}
|
||||
|
||||
setStackName(name) {
|
||||
this.formValues.StackName = name;
|
||||
return this.$async(async () => {
|
||||
if (KUBE_STACK_NAME_VALIDATION_REGEX.test(name) || name === '') {
|
||||
this.state.stackNameError = '';
|
||||
} else {
|
||||
this.state.stackNameError =
|
||||
"Stack must consist of alphanumeric characters, '-', '_' or '.', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123').";
|
||||
}
|
||||
|
||||
this.formValues.StackName = name;
|
||||
});
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
|
@ -197,9 +208,9 @@ class KubernetesDeployController {
|
|||
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent);
|
||||
const isURLFormInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.URL && _.isEmpty(this.formValues.ManifestURL);
|
||||
const isCustomTemplateInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.CUSTOM_TEMPLATE && _.isEmpty(this.formValues.EditorContent);
|
||||
|
||||
const isStackNameInvalid = this.state.stackNameError !== '';
|
||||
const isNamespaceInvalid = _.isEmpty(this.formValues.Namespace);
|
||||
return isWebEditorInvalid || isURLFormInvalid || isCustomTemplateInvalid || this.state.actionInProgress || isNamespaceInvalid;
|
||||
return isWebEditorInvalid || isURLFormInvalid || isCustomTemplateInvalid || this.state.actionInProgress || isNamespaceInvalid || isStackNameInvalid;
|
||||
}
|
||||
|
||||
onChangeFormValues(newValues) {
|
||||
|
|
|
@ -1,86 +1,84 @@
|
|||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="$ctrl.template.Logo" title-text="$ctrl.template.Title"></rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal" name="stackTemplateForm">
|
||||
<!-- description -->
|
||||
<div ng-if="$ctrl.template.Note">
|
||||
<div class="form-section-title"> Information </div>
|
||||
<div class="col-sm-12 form-group">
|
||||
<div class="template-note" ng-bind-html="$ctrl.template.Note"></div>
|
||||
</div>
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="$ctrl.template.Logo" title-text="$ctrl.template.Title"></rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal" name="stackTemplateForm">
|
||||
<!-- description -->
|
||||
<div ng-if="$ctrl.template.Note">
|
||||
<div class="form-section-title"> Information </div>
|
||||
<div class="col-sm-12 form-group">
|
||||
<div class="template-note" ng-bind-html="$ctrl.template.Note"></div>
|
||||
</div>
|
||||
<!-- !description -->
|
||||
<div class="form-section-title"> Configuration </div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="template_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="template_name" class="form-control" ng-model="$ctrl.formValues.name" ng-pattern="$ctrl.nameRegex" placeholder="e.g. myStack" required />
|
||||
<div class="form-group" ng-if="stackTemplateForm.template_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="stackTemplateForm.template_name.$error">
|
||||
<p ng-message="pattern" class="vertical-center">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
<span>This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span>
|
||||
</p>
|
||||
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>This field is required. </p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !description -->
|
||||
<div class="form-section-title"> Configuration </div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="template_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="template_name" class="form-control" ng-model="$ctrl.formValues.name" ng-pattern="$ctrl.nameRegex" placeholder="e.g. mystack" required />
|
||||
<div class="form-group" ng-if="stackTemplateForm.template_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="stackTemplateForm.template_name.$error">
|
||||
<p ng-message="pattern" class="vertical-center">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
<span>This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span>
|
||||
</p>
|
||||
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>This field is required. </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- !name-input -->
|
||||
<!-- env -->
|
||||
<div ng-repeat="var in $ctrl.template.Env" ng-if="!var.preset || var.select" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">
|
||||
{{ var.label }}
|
||||
<portainer-tooltip ng-if="var.description" message="var.description"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}" />
|
||||
<select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}">
|
||||
<option selected disabled hidden value="">Select value</option>
|
||||
<option ng-repeat="choice in var.select" value="{{ choice.value }}">{{ choice.text }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- env -->
|
||||
<div ng-repeat="var in $ctrl.template.Env" ng-if="!var.preset || var.select" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">
|
||||
{{ var.label }}
|
||||
<portainer-tooltip ng-if="var.description" message="var.description"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}" />
|
||||
<select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}">
|
||||
<option selected disabled hidden value="">Select value</option>
|
||||
<option ng-repeat="choice in var.select" value="{{ choice.value }}">{{ choice.text }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- !env -->
|
||||
<ng-transclude ng-transclude-slot="advanced"></ng-transclude>
|
||||
</div>
|
||||
<!-- !env -->
|
||||
<ng-transclude ng-transclude-slot="advanced"></ng-transclude>
|
||||
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="form-section-title"> Actions </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.name || !$ctrl.state.deployable || stackTemplateForm.$invalid"
|
||||
ng-click="$ctrl.createTemplate()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" ng-click="$ctrl.unselectTemplate($ctrl.template)">Hide</button>
|
||||
<div class="form-group" ng-if="$ctrl.state.formValidationError">
|
||||
<div class="col-sm-12 small text-danger" ng-if="$ctrl.state.formValidationError">
|
||||
<p class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>{{ $ctrl.state.formValidationError }} </p>
|
||||
</div>
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="form-section-title"> Actions </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.name || !$ctrl.state.deployable || stackTemplateForm.$invalid"
|
||||
ng-click="$ctrl.createTemplate()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" ng-click="$ctrl.unselectTemplate($ctrl.template)">Hide</button>
|
||||
<div class="form-group" ng-if="$ctrl.state.formValidationError">
|
||||
<div class="col-sm-12 small text-danger" ng-if="$ctrl.state.formValidationError">
|
||||
<p class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>{{ $ctrl.state.formValidationError }} </p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="!$ctrl.state.deployable">
|
||||
<div class="col-sm-12 small text-danger" ng-if="!$ctrl.state.deployable">
|
||||
<p class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>This template type cannot be deployed on this environment. </p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="!$ctrl.state.deployable">
|
||||
<div class="col-sm-12 small text-danger" ng-if="!$ctrl.state.deployable">
|
||||
<p class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>This template type cannot be deployed on this environment. </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -1,66 +1,68 @@
|
|||
<page-header title="'Custom Templates'" breadcrumbs="['Custom Templates']" reload="true"> </page-header>
|
||||
|
||||
<div class="row">
|
||||
<stack-from-template-form
|
||||
ng-if="$ctrl.state.selectedTemplate"
|
||||
template="$ctrl.state.selectedTemplate"
|
||||
form-values="$ctrl.formValues"
|
||||
name-regex="$ctrl.state.templateNameRegex"
|
||||
state="$ctrl.state"
|
||||
create-template="$ctrl.createStack"
|
||||
unselect-template="$ctrl.unselectTemplate"
|
||||
>
|
||||
<advanced-form>
|
||||
<custom-templates-variables-field
|
||||
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||
definitions="$ctrl.state.selectedTemplate.Variables"
|
||||
value="$ctrl.formValues.variables"
|
||||
on-change="($ctrl.onChangeTemplateVariables)"
|
||||
></custom-templates-variables-field>
|
||||
<div class="col-sm-12">
|
||||
<stack-from-template-form
|
||||
ng-if="$ctrl.state.selectedTemplate"
|
||||
template="$ctrl.state.selectedTemplate"
|
||||
form-values="$ctrl.formValues"
|
||||
name-regex="$ctrl.state.templateNameRegex"
|
||||
state="$ctrl.state"
|
||||
create-template="$ctrl.createStack"
|
||||
unselect-template="$ctrl.unselectTemplate"
|
||||
>
|
||||
<advanced-form>
|
||||
<custom-templates-variables-field
|
||||
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||
definitions="$ctrl.state.selectedTemplate.Variables"
|
||||
value="$ctrl.formValues.variables"
|
||||
on-change="($ctrl.onChangeTemplateVariables)"
|
||||
></custom-templates-variables-field>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.state.selectedTemplate && !$ctrl.state.templateLoadFailed">
|
||||
<div class="col-sm-12">
|
||||
<a class="small interactive vertical-center" ng-show="!$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = true;">
|
||||
<pr-icon icon="'plus'" class-name="space-right" feather="true"></pr-icon> {{ $ctrl.state.selectedTemplate.GitConfig !== null ? 'View' : 'Customize' }} stack
|
||||
</a>
|
||||
<a class="small interactive vertical-center" ng-show="$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = false;">
|
||||
<pr-icon icon="'minus'" class-name="space-right" feather="true"></pr-icon> Hide {{ $ctrl.state.selectedTemplate.GitConfig === null ? 'custom' : '' }} stack
|
||||
</a>
|
||||
<div class="form-group" ng-if="$ctrl.state.selectedTemplate && !$ctrl.state.templateLoadFailed">
|
||||
<div class="col-sm-12">
|
||||
<a class="small interactive vertical-center" ng-show="!$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = true;">
|
||||
<pr-icon icon="'plus'" class-name="space-right" feather="true"></pr-icon> {{ $ctrl.state.selectedTemplate.GitConfig !== null ? 'View' : 'Customize' }} stack
|
||||
</a>
|
||||
<a class="small interactive vertical-center" ng-show="$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = false;">
|
||||
<pr-icon icon="'minus'" class-name="space-right" feather="true"></pr-icon> Hide {{ $ctrl.state.selectedTemplate.GitConfig === null ? 'custom' : '' }} stack
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span ng-if="$ctrl.state.selectedTemplate && $ctrl.state.templateLoadFailed">
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="$ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
|
||||
<a ui-sref="docker.templates.custom.edit({id: $ctrl.state.selectedTemplate.Id})">click here</a> for configuration.</p
|
||||
>
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="!($ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId)">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
|
||||
>
|
||||
</span>
|
||||
<span ng-if="$ctrl.state.selectedTemplate && $ctrl.state.templateLoadFailed">
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="$ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
|
||||
<a ui-sref="docker.templates.custom.edit({id: $ctrl.state.selectedTemplate.Id})">click here</a> for configuration.</p
|
||||
>
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="!($ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId)">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
|
||||
>
|
||||
</span>
|
||||
|
||||
<!-- web-editor -->
|
||||
<web-editor-form
|
||||
ng-if="$ctrl.state.showAdvancedOptions"
|
||||
identifier="custom-template-creation-editor"
|
||||
value="$ctrl.formValues.fileContent"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
read-only="$ctrl.state.isEditorReadOnly"
|
||||
>
|
||||
<editor-description>
|
||||
<p>
|
||||
You can get more information about Compose file format in the
|
||||
<a href="https://docs.docker.com/compose/compose-file/" target="_blank"> official documentation </a>
|
||||
.
|
||||
</p>
|
||||
</editor-description>
|
||||
</web-editor-form>
|
||||
<!-- !web-editor -->
|
||||
</advanced-form>
|
||||
</stack-from-template-form>
|
||||
<!-- web-editor -->
|
||||
<web-editor-form
|
||||
ng-if="$ctrl.state.showAdvancedOptions"
|
||||
identifier="custom-template-creation-editor"
|
||||
value="$ctrl.formValues.fileContent"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
read-only="$ctrl.state.isEditorReadOnly"
|
||||
>
|
||||
<editor-description>
|
||||
<p>
|
||||
You can get more information about Compose file format in the
|
||||
<a href="https://docs.docker.com/compose/compose-file/" target="_blank"> official documentation </a>
|
||||
.
|
||||
</p>
|
||||
</editor-description>
|
||||
</web-editor-form>
|
||||
<!-- !web-editor -->
|
||||
</advanced-form>
|
||||
</stack-from-template-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<custom-templates-list
|
||||
|
|
|
@ -218,7 +218,7 @@ class CustomTemplatesViewController {
|
|||
return o.Name === 'bridge';
|
||||
});
|
||||
|
||||
this.formValues.name = template.Title ? template.Title : '';
|
||||
this.formValues.name = '';
|
||||
this.state.selectedTemplate = template;
|
||||
this.$anchorScroll('view-top');
|
||||
const applicationState = this.StateManager.getState();
|
||||
|
|
|
@ -2,15 +2,18 @@
|
|||
|
||||
<div class="row">
|
||||
<!-- stack-form -->
|
||||
<stack-from-template-form
|
||||
ng-if="state.selectedTemplate && (state.selectedTemplate.Type === 2 || state.selectedTemplate.Type === 3)"
|
||||
template="state.selectedTemplate"
|
||||
form-values="formValues"
|
||||
state="state"
|
||||
create-template="createTemplate"
|
||||
unselect-template="unselectTemplate"
|
||||
>
|
||||
</stack-from-template-form>
|
||||
<div class="col-sm-12">
|
||||
<stack-from-template-form
|
||||
ng-if="state.selectedTemplate && (state.selectedTemplate.Type === 2 || state.selectedTemplate.Type === 3)"
|
||||
template="state.selectedTemplate"
|
||||
form-values="formValues"
|
||||
name-regex="state.templateNameRegex"
|
||||
state="state"
|
||||
create-template="createTemplate"
|
||||
unselect-template="unselectTemplate"
|
||||
>
|
||||
</stack-from-template-form>
|
||||
</div>
|
||||
<!-- !stack-form -->
|
||||
<!-- container-form -->
|
||||
<div class="col-sm-12" ng-if="state.selectedTemplate && state.selectedTemplate.Type === 1">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash-es';
|
||||
import { TemplateType } from '@/react/portainer/templates/app-templates/types';
|
||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||
import { AccessControlFormData } from '../../components/accessControlForm/porAccessControlFormModel';
|
||||
|
||||
angular.module('portainer.app').controller('TemplatesController', [
|
||||
|
@ -47,6 +48,7 @@ angular.module('portainer.app').controller('TemplatesController', [
|
|||
showAdvancedOptions: false,
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
};
|
||||
|
||||
$scope.enabledTypes = [TemplateType.Container, TemplateType.ComposeStack];
|
||||
|
|
|
@ -33,7 +33,7 @@ export const textByType = {
|
|||
(Deployment, Secret, ConfigMap...)
|
||||
</p>
|
||||
<p>
|
||||
You can get more information about Kubernetes file format in the
|
||||
You can get more information about Kubernetes file format in the{' '}
|
||||
<a
|
||||
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/"
|
||||
target="_blank"
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Link } from '@@/Link';
|
|||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
import { AutocompleteSelect } from '@@/form-components/AutocompleteSelect';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
|
||||
type Props = {
|
||||
stackName: string;
|
||||
|
@ -13,6 +14,7 @@ type Props = {
|
|||
stacks?: string[];
|
||||
inputClassName?: string;
|
||||
textTip?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export function StackName({
|
||||
|
@ -21,6 +23,7 @@ export function StackName({
|
|||
stacks = [],
|
||||
inputClassName,
|
||||
textTip = "Enter or select a 'stack' name to group multiple deployments together, or else leave empty to ignore.",
|
||||
error = '',
|
||||
}: Props) {
|
||||
const isAdminQuery = useIsEdgeAdmin();
|
||||
const stackResults = useMemo(
|
||||
|
@ -50,9 +53,11 @@ export function StackName({
|
|||
|
||||
return (
|
||||
<>
|
||||
<TextTip className="mb-4" color="blue">
|
||||
{textTip}
|
||||
</TextTip>
|
||||
{textTip ? (
|
||||
<TextTip className="mb-4" color="blue">
|
||||
{textTip}
|
||||
</TextTip>
|
||||
) : null}
|
||||
<div className="form-group">
|
||||
<label
|
||||
htmlFor="stack_name"
|
||||
|
@ -72,6 +77,7 @@ export function StackName({
|
|||
placeholder="e.g. myStack"
|
||||
inputId="stack_name"
|
||||
/>
|
||||
{error ? <FormError>{error}</FormError> : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
// this regex is to satisfy k8s label validation rules
|
||||
// alphanumeric, lowercase, uppercase, can contain dashes, dots and underscores, max 63 characters
|
||||
export const KUBE_STACK_NAME_VALIDATION_REGEX =
|
||||
/^(([a-zA-Z0-9](?:(?:[-a-zA-Z0-9_.]){0,61}[a-zA-Z0-9])?))$/;
|
|
@ -91,14 +91,10 @@ export function CommonFields({
|
|||
export function validation({
|
||||
currentTemplateId,
|
||||
templates = [],
|
||||
viewType = 'docker',
|
||||
}: {
|
||||
currentTemplateId?: CustomTemplate['Id'];
|
||||
templates?: Array<CustomTemplate>;
|
||||
viewType?: 'kube' | 'docker' | 'edge';
|
||||
} = {}): SchemaOf<Values> {
|
||||
const titlePattern = titlePatternValidation(viewType);
|
||||
|
||||
return object({
|
||||
Title: string()
|
||||
.required('Title is required.')
|
||||
|
@ -112,7 +108,10 @@ export function validation({
|
|||
template.Title === value && template.Id !== currentTemplateId
|
||||
)
|
||||
)
|
||||
.matches(titlePattern.pattern, titlePattern.error),
|
||||
.max(
|
||||
200,
|
||||
'Custom template title must be less than or equal to 200 characters'
|
||||
),
|
||||
Description: string().required('Description is required.'),
|
||||
Note: string().default(''),
|
||||
Logo: string().default(''),
|
||||
|
@ -120,23 +119,3 @@ export function validation({
|
|||
}
|
||||
|
||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
|
||||
const KUBE_TEMPLATE_NAME_VALIDATION_REGEX =
|
||||
'^(([a-z0-9](?:(?:[-a-z0-9_.]){0,61}[a-z0-9])?))$'; // alphanumeric, lowercase, can contain dashes, dots and underscores, max 63 characters
|
||||
|
||||
function titlePatternValidation(type: 'kube' | 'docker' | 'edge') {
|
||||
switch (type) {
|
||||
case 'kube':
|
||||
return {
|
||||
pattern: new RegExp(KUBE_TEMPLATE_NAME_VALIDATION_REGEX),
|
||||
error:
|
||||
"This field must consist of lower-case alphanumeric characters, '.', '_' or '-', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123').",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
pattern: new RegExp(TEMPLATE_NAME_VALIDATION_REGEX),
|
||||
error:
|
||||
"This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MetadataFieldset } from './MetadataFieldset';
|
|||
|
||||
export function MoreSettingsSection({ children }: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<FormSection title="More settings" isFoldable>
|
||||
<FormSection title="More settings" className="ml-0" isFoldable>
|
||||
<div className="ml-8">
|
||||
{children}
|
||||
|
||||
|
|
|
@ -65,7 +65,6 @@ export function useValidation({
|
|||
}).concat(
|
||||
commonFieldsValidation({
|
||||
templates: customTemplatesQuery.data,
|
||||
viewType,
|
||||
})
|
||||
),
|
||||
[customTemplatesQuery.data, gitCredentialsQuery.data, viewType]
|
||||
|
|
|
@ -55,7 +55,6 @@ export function useValidation({
|
|||
commonFieldsValidation({
|
||||
templates: customTemplatesQuery.data,
|
||||
currentTemplateId: templateId,
|
||||
viewType,
|
||||
})
|
||||
),
|
||||
[
|
||||
|
|
Loading…
Reference in New Issue