From cf0a289c429e3ce950c893da14f6f2a748a4354b Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Fri, 20 Mar 2026 10:20:31 +0100 Subject: [PATCH] EDM-3750: Validate catalogItem versions correctly --- libs/i18n/locales/en/translation.json | 6 +- libs/types/alpha/models/ApiVersion.ts | 4 +- libs/types/alpha/models/CatalogItemVersion.ts | 2 +- libs/types/models/K8sProviderSpec.ts | 10 +- .../Catalog/AddCatalogItemWizard/utils.ts | 97 ++++++++++++++----- libs/ui-components/src/constants.ts | 2 +- 6 files changed, 89 insertions(+), 32 deletions(-) diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 9c158f3db..1860499a0 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -264,9 +264,9 @@ "Remove version": "Remove version", "Add version": "Add version", "Must be a valid JSON Schema (JSON or YAML)": "Must be a valid JSON Schema (JSON or YAML)", - "Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)": "Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)", - "Each entry must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)": "Each entry must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)", - "Must be a valid semver range (e.g. >=1.0.0 <2.0.0)": "Must be a valid semver range (e.g. >=1.0.0 <2.0.0)", + "Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.": "Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.", + "Each entry must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.": "Each entry must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading \"v\" is not allowed.", + "Must be a valid semver range (e.g. >=1.0.0 <2.0.0). Leading \"v\" is not allowed.": "Must be a valid semver range (e.g. >=1.0.0 <2.0.0). Leading \"v\" is not allowed.", "Must be a valid YAML or JSON object": "Must be a valid YAML or JSON object", "Version is required": "Version is required", "Version must be unique": "Version must be unique", diff --git a/libs/types/alpha/models/ApiVersion.ts b/libs/types/alpha/models/ApiVersion.ts index d1e05a873..7212533da 100644 --- a/libs/types/alpha/models/ApiVersion.ts +++ b/libs/types/alpha/models/ApiVersion.ts @@ -6,6 +6,6 @@ * APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources. */ export enum ApiVersion { - V1ALPHA1 = 'v1alpha1', - FLIGHTCTL_IO_V1ALPHA1 = 'flightctl.io/v1alpha1', + ApiVersionV1alpha1 = 'v1alpha1', + ApiVersionFlightctlIoV1alpha1 = 'flightctl.io/v1alpha1', } diff --git a/libs/types/alpha/models/CatalogItemVersion.ts b/libs/types/alpha/models/CatalogItemVersion.ts index 4db4bec58..d20cc3734 100644 --- a/libs/types/alpha/models/CatalogItemVersion.ts +++ b/libs/types/alpha/models/CatalogItemVersion.ts @@ -14,7 +14,7 @@ import type { CatalogItemDeprecation } from './CatalogItemDeprecation'; */ export type CatalogItemVersion = (CatalogItemConfigurable & { /** - * Semantic version identifier (e.g., 1.2.3, v2.0.0-rc1). Required for version ordering and upgrade graph. + * Semantic version identifier (e.g., 1.2.3, 2.0.0-rc1). Required for version ordering and upgrade graph. */ version: string; /** diff --git a/libs/types/models/K8sProviderSpec.ts b/libs/types/models/K8sProviderSpec.ts index cad8009ef..4897751eb 100644 --- a/libs/types/models/K8sProviderSpec.ts +++ b/libs/types/models/K8sProviderSpec.ts @@ -28,8 +28,14 @@ export type K8sProviderSpec = { * Whether this K8s provider is enabled. */ enabled?: boolean; - organizationAssignment: AuthOrganizationAssignment; - roleAssignment: AuthRoleAssignment; + /** + * How users from this auth provider are assigned to organizations. + */ + organizationAssignment: AuthOrganizationAssignment | null; + /** + * How users from this auth provider are assigned roles. + */ + roleAssignment: AuthRoleAssignment | null; /** * Optional suffix to strip from ClusterRole names when normalizing role names. Used for multi-release deployments where ClusterRoles have namespace-specific names (e.g., flightctl-admin-). */ diff --git a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts index 147bd36cb..e4b14dfe7 100644 --- a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts +++ b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/utils.ts @@ -111,7 +111,7 @@ const formVersionsToApi = (values: AddCatalogItemFormValues) => { channels: v.channels, replaces: v.replaces || undefined, skips: skips.length ? skips : undefined, - skipRange: v.skipRange || undefined, + skipRange: toApiCatalogSkipRange(v.skipRange || ''), readme: v.readme || undefined, config: configurable ? parseYamlField(v.config) : undefined, configSchema: configurable ? parseSchemaField(v.configSchema) : undefined, @@ -175,7 +175,7 @@ export const getCatalogItemResource = (values: AddCatalogItemFormValues, catalog const type = values.type as CatalogItemType; return { - apiVersion: ApiVersion.V1ALPHA1, + apiVersion: ApiVersion.ApiVersionV1alpha1, kind: 'CatalogItem', metadata: { name: values.name, @@ -345,18 +345,61 @@ const jsonSchemaFieldSchema = (t: TFunction) => } }); -const optionalSemver = (t: TFunction) => - Yup.string().test('valid-semver', t('Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)'), (value) => { - if (!value) { - return true; +const hasForbiddenLeadingVPrefix = (s: string): boolean => s.trimStart().startsWith('v'); + +const isValidCatalogSingleSemver = (value: string): boolean => + !hasForbiddenLeadingVPrefix(value) && semver.valid(value) !== null; + +const RANGE_OPERATOR_TRIM = /^[=<>~^]+/; + +// Ensure that no leading "v" are present. +// Allow the user to type extra spaces after operators; they will be removed when the range is saved. +// Valid examples: ">= 1.0.0 < 2.0.0", ">=1.0.0 <2.0.0" +const isValidCatalogSemverRange = (value: string): boolean => { + const trimmed = value.trim(); + if (!trimmed) { + return false; + } + const normalizedRange = semver.validRange(trimmed); + if (normalizedRange === null) { + return false; + } + const versionParts = normalizedRange.split(/\s+/); + for (const part of versionParts) { + const version = part.replace(RANGE_OPERATOR_TRIM, ''); + if (!version || hasForbiddenLeadingVPrefix(version)) { + return false; } - return semver.valid(value) !== null; - }); + } + return true; +}; + +// Normalize the skip range for the API. Spaces after operators must be removed. +const toApiCatalogSkipRange = (value: string): string | undefined => { + const trimmed = value.trim(); + if (!trimmed) { + return undefined; + } + const normalized = semver.validRange(trimmed); + return normalized ?? undefined; +}; + +const optionalSemver = (t: TFunction) => + Yup.string().test( + 'valid-semver', + t('Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading "v" is not allowed.'), + (value) => { + if (!value) { + return true; + } + return isValidCatalogSingleSemver(value); + }, + ); const optionalSemverList = (t: TFunction) => Yup.string().test( 'valid-semver-list', - t('Each entry must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)'), + t('Each entry must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading "v" is not allowed.'), (value) => { if (!value?.trim()) { return true; @@ -365,17 +408,21 @@ const optionalSemverList = (t: TFunction) => .split(',') .map((s) => s.trim()) .filter(Boolean) - .every((v) => semver.valid(v) !== null); + .every(isValidCatalogSingleSemver); }, ); const optionalSemverRange = (t: TFunction) => - Yup.string().test('valid-semver-range', t('Must be a valid semver range (e.g. >=1.0.0 <2.0.0)'), (value) => { - if (!value?.trim()) { - return true; - } - return semver.validRange(value) !== null; - }); + Yup.string().test( + 'valid-semver-range', + t('Must be a valid semver range (e.g. >=1.0.0 <2.0.0). Leading "v" is not allowed.'), + (value) => { + if (!value?.trim()) { + return true; + } + return isValidCatalogSemverRange(value); + }, + ); const yamlFieldSchema = (t: TFunction) => Yup.string().test('valid-yaml-json', t('Must be a valid YAML or JSON object'), (value) => { @@ -394,12 +441,16 @@ const versionSchema = (t: TFunction, duplicates: Set, configurable: bool Yup.object().shape({ version: Yup.string() .required(t('Version is required')) - .test('valid-semver', t('Must be a valid semantic version (e.g. 1.0.0, v2.1.0-rc1)'), (value) => { - if (!value) { - return true; - } - return semver.valid(value) !== null; - }) + .test( + 'valid-semver', + t('Must be a valid semantic version (e.g. 1.0.0, 2.1.0-rc1). Leading "v" is not allowed.'), + (value) => { + if (!value) { + return true; + } + return isValidCatalogSingleSemver(value); + }, + ) .test('unique-version', t('Version must be unique'), (value) => { if (!value) { return true; @@ -642,7 +693,7 @@ export const getCatalogPatches = (catalog: Catalog, values: CreateCatalogFormVal }; export const getCatalogResource = (values: CreateCatalogFormValues): Catalog => ({ - apiVersion: ApiVersion.V1ALPHA1, + apiVersion: ApiVersion.ApiVersionV1alpha1, kind: 'Catalog', metadata: { name: values.name }, spec: { diff --git a/libs/ui-components/src/constants.ts b/libs/ui-components/src/constants.ts index fb5dee401..04169de0f 100644 --- a/libs/ui-components/src/constants.ts +++ b/libs/ui-components/src/constants.ts @@ -15,7 +15,7 @@ export const getApiVersion = (api: 'flightctl' | 'imagebuilder' | 'alerts' | 'ca case 'imagebuilder': return ImageBuilderApiVersion.ApiVersionV1alpha1; case 'catalog': - return AlphaVersion.V1ALPHA1; + return AlphaVersion.ApiVersionV1alpha1; case 'alerts': default: return undefined;