Skip to content

Commit

Permalink
fix(custom-templates): add stack validation, remove custom template v…
Browse files Browse the repository at this point in the history
…alidation [EE-7102] (#11938)

Co-authored-by: testa113 <testa113>
  • Loading branch information
testA113 committed Jun 16, 2024
1 parent 0f5988a commit be9d328
Show file tree
Hide file tree
Showing 15 changed files with 68 additions and 44 deletions.
9 changes: 8 additions & 1 deletion app/kubernetes/react/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,14 @@ export const ngModule = angular
),
{ stackName: 'setStackName' }
),
['setStackName', 'stackName', 'stacks', 'inputClassName', 'textTip']
[
'setStackName',
'stackName',
'stacks',
'inputClassName',
'textTip',
'error',
]
)
)
.component(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->

Expand Down Expand Up @@ -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 -->

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
});
}

Expand Down Expand Up @@ -649,7 +658,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() {
Expand Down
7 changes: 6 additions & 1 deletion app/kubernetes/views/deploy/deploy.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@
<div class="mb-4 w-fit">
<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 -->

Expand Down
17 changes: 14 additions & 3 deletions app/kubernetes/views/deploy/deployController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -57,6 +58,7 @@ class KubernetesDeployController {
templateLoadFailed: false,
isEditorReadOnly: false,
selectedHelmChart: '',
stackNameError: '',
};

this.currentUser = {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 isNamespaceInvalid = _.isEmpty(this.formValues.Namespace);
return isWebEditorInvalid || isURLFormInvalid || isCustomTemplateInvalid || this.state.actionInProgress || isNamespaceInvalid;
const isStackNameInvalid = this.state.stackNameError !== '';
return isWebEditorInvalid || isURLFormInvalid || isCustomTemplateInvalid || this.state.actionInProgress || isNamespaceInvalid || isStackNameInvalid;
}

onChangeFormValues(newValues) {
Expand Down
3 changes: 3 additions & 0 deletions app/react/common/stacks/CreateView/NameField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ export function NameField({
onChange,
value,
errors,
placeholder,
}: {
onChange(value: string): void;
value: string;
errors?: FormikErrors<string>;
placeholder?: string;
}) {
return (
<FormControl inputId="name-input" label="Name" errors={errors} required>
<Input
id="name-input"
onChange={(e) => onChange(e.target.value)}
value={value}
placeholder={placeholder}
required
data-cy="stack-name-input"
/>
Expand Down
2 changes: 1 addition & 1 deletion app/react/common/stacks/common/form-texts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion app/react/components/CollapseExpandButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function CollapseExpandButton({
aria-label={isExpanded ? 'Collapse' : 'Expand'}
aria-expanded={isExpanded}
type="button"
className="flex-none border-none bg-transparent flex items-center p-0 px-3 group"
className="flex-none border-none bg-transparent flex items-center p-0 !ml-0 group"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
Expand Down
12 changes: 9 additions & 3 deletions app/react/kubernetes/DeployView/StackName/StackName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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;
setStackName: (name: string) => void;
stacks?: string[];
inputClassName?: string;
textTip?: string;
error?: string;
};

export function StackName({
Expand All @@ -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(
Expand Down Expand Up @@ -54,9 +57,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"
Expand All @@ -77,6 +82,7 @@ export function StackName({
inputId="stack_name"
data-cy="k8s-deploy-stack-input"
/>
{error ? <FormError>{error}</FormError> : null}
</div>
</div>
</>
Expand Down
4 changes: 4 additions & 0 deletions app/react/kubernetes/DeployView/StackName/constants.ts
Original file line number Diff line number Diff line change
@@ -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])?))$/;
31 changes: 4 additions & 27 deletions app/react/portainer/custom-templates/components/CommonFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,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.')
Expand All @@ -116,31 +112,12 @@ 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(''),
});
}

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').",
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export function useValidation({
}).concat(
commonFieldsValidation({
templates: customTemplatesQuery.data,
viewType,
})
),
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export function useValidation({
commonFieldsValidation({
templates: customTemplatesQuery.data,
currentTemplateId: templateId,
viewType,
})
),
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function DeployForm({
const isGit = !!template.GitConfig;

const initialValues: FormValues = {
name: template.Title || '',
name: '',
variables: getVariablesFieldDefaultValues(template.Variables),
accessControl: parseAccessControlFormData(
isEdgeAdminQuery.isAdmin,
Expand All @@ -86,6 +86,7 @@ export function DeployForm({
value={values.name}
onChange={(v) => setFieldValue('name', v)}
errors={errors.name}
placeholder="e.g. mystack"
/>
</FormSection>

Expand Down

0 comments on commit be9d328

Please sign in to comment.