From ed4e62561f7eff5992fb5f0c5e8e37b58409becc Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 15 May 2025 12:51:51 +0200 Subject: [PATCH 1/5] fix(catalog-model): add support for icons containing colons Signed-off-by: Benjamin Janssens --- .changeset/dull-banks-shake.md | 5 +++++ .../policies/FieldFormatEntityPolicy.test.ts | 16 +++++++------- .../policies/FieldFormatEntityPolicy.ts | 6 +++++- .../CommonValidatorFunctions.test.ts | 21 +++++++++++++++++++ .../validation/CommonValidatorFunctions.ts | 14 +++++++++++++ 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 .changeset/dull-banks-shake.md diff --git a/.changeset/dull-banks-shake.md b/.changeset/dull-banks-shake.md new file mode 100644 index 0000000000..1882cf4f42 --- /dev/null +++ b/.changeset/dull-banks-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Added support for icons containing colons diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 9fe041a839..1769503fe0 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -189,13 +189,15 @@ describe('FieldFormatEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*""/i); }); - it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])( - 'accepts valid link icon', - async icon => { - data.metadata.links = [{ url: 'http://foo', icon }]; - await expect(policy.enforce(data)).resolves.toBe(data); - }, - ); + it.each([ + ['dashboard'], + ['admin-dashboard'], + ['foo_dashboard'], + ['kind:api'], + ])('accepts valid link icon', async icon => { + data.metadata.links = [{ url: 'http://foo', icon }]; + await expect(policy.enforce(data)).resolves.toBe(data); + }); it.each([[123], [{}], [[]], ['abc xyz']])( 'rejects bad link icon value %s', diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 18e9523478..c3f2802258 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -97,6 +97,10 @@ export class FieldFormatEntityPolicy implements EntityPolicy { case 'isNonEmptyString': expectation = 'a non empty string'; break; + case 'isValidIcon': + expectation = + 'a string that is sequences of [a-zA-Z0-9] separated by any of [-_.:], at most 63 characters in total'; + break; default: expectation = undefined; break; @@ -161,7 +165,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { optional( `links.${i}.icon`, links[i]?.icon, - KubernetesValidatorFunctions.isValidObjectName, + CommonValidatorFunctions.isValidIcon, ); } diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 88b6e54113..66ab9f0932 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -243,4 +243,25 @@ describe('CommonValidatorFunctions', () => { ])(`isNonEmptyString %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result); }); + + it.each([ + [7, false], + [null, false], + ['', false], + ['a', true], + ['AZ09', true], + ['a'.repeat(63), true], + ['a'.repeat(64), false], + ['a/b', false], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', true], + ['a_b', true], + ['a.b', true], + ['a..b', true], + ['a:b', true], + ])(`isValidIcon %p ? %p`, (value, matches) => { + expect(CommonValidatorFunctions.isValidIcon(value)).toBe(matches); + }); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 295c5b8895..9a685094c8 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -147,4 +147,18 @@ export class CommonValidatorFunctions { static isNonEmptyString(value: unknown): value is string { return typeof value === 'string' && value?.trim()?.length >= 1; } + + /** + * Checks that the value is a valid icon. + * + * @param value - The value to check + */ + static isValidIcon(value: unknown): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^([A-Za-z0-9][-A-Za-z0-9_.:]*)?[A-Za-z0-9]$/.test(value) + ); + } } From 026b8cb3c93e715cd43a65ecce5675e615352a4a Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 15 May 2025 13:18:57 +0200 Subject: [PATCH 2/5] chore(catalog-model): generate api reports Signed-off-by: Benjamin Janssens --- packages/catalog-model/report.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/catalog-model/report.api.md b/packages/catalog-model/report.api.md index ec03ce41e1..8944a51944 100644 --- a/packages/catalog-model/report.api.md +++ b/packages/catalog-model/report.api.md @@ -59,6 +59,7 @@ export class CommonValidatorFunctions { static isNonEmptyString(value: unknown): value is string; static isValidDnsLabel(value: unknown): boolean; static isValidDnsSubdomain(value: unknown): boolean; + static isValidIcon(value: unknown): boolean; static isValidPrefixAndOrSuffix( value: unknown, separator: string, From b1cf2a90596d25af24c0eb9106c63e263b7db678 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 16 May 2025 13:38:53 +0200 Subject: [PATCH 3/5] Revert "chore(catalog-model): generate api reports" This reverts commit 5f7b488b223b7b7d71ea39eb31209208dbe38568. Signed-off-by: Benjamin Janssens --- packages/catalog-model/report.api.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/catalog-model/report.api.md b/packages/catalog-model/report.api.md index 8944a51944..ec03ce41e1 100644 --- a/packages/catalog-model/report.api.md +++ b/packages/catalog-model/report.api.md @@ -59,7 +59,6 @@ export class CommonValidatorFunctions { static isNonEmptyString(value: unknown): value is string; static isValidDnsLabel(value: unknown): boolean; static isValidDnsSubdomain(value: unknown): boolean; - static isValidIcon(value: unknown): boolean; static isValidPrefixAndOrSuffix( value: unknown, separator: string, From aeb70a02e2e4e78a91a3043869253a33d80c7476 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 16 May 2025 13:40:01 +0200 Subject: [PATCH 4/5] Revert "fix(catalog-model): add support for icons containing colons" This reverts commit b902d521aa451c2222f6cb7294aa6d74ed3f2a99. Signed-off-by: Benjamin Janssens --- .../policies/FieldFormatEntityPolicy.test.ts | 16 +++++++------- .../policies/FieldFormatEntityPolicy.ts | 6 +----- .../CommonValidatorFunctions.test.ts | 21 ------------------- .../validation/CommonValidatorFunctions.ts | 14 ------------- 4 files changed, 8 insertions(+), 49 deletions(-) diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 1769503fe0..9fe041a839 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -189,15 +189,13 @@ describe('FieldFormatEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*""/i); }); - it.each([ - ['dashboard'], - ['admin-dashboard'], - ['foo_dashboard'], - ['kind:api'], - ])('accepts valid link icon', async icon => { - data.metadata.links = [{ url: 'http://foo', icon }]; - await expect(policy.enforce(data)).resolves.toBe(data); - }); + it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])( + 'accepts valid link icon', + async icon => { + data.metadata.links = [{ url: 'http://foo', icon }]; + await expect(policy.enforce(data)).resolves.toBe(data); + }, + ); it.each([[123], [{}], [[]], ['abc xyz']])( 'rejects bad link icon value %s', diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index c3f2802258..18e9523478 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -97,10 +97,6 @@ export class FieldFormatEntityPolicy implements EntityPolicy { case 'isNonEmptyString': expectation = 'a non empty string'; break; - case 'isValidIcon': - expectation = - 'a string that is sequences of [a-zA-Z0-9] separated by any of [-_.:], at most 63 characters in total'; - break; default: expectation = undefined; break; @@ -165,7 +161,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { optional( `links.${i}.icon`, links[i]?.icon, - CommonValidatorFunctions.isValidIcon, + KubernetesValidatorFunctions.isValidObjectName, ); } diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 66ab9f0932..88b6e54113 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -243,25 +243,4 @@ describe('CommonValidatorFunctions', () => { ])(`isNonEmptyString %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result); }); - - it.each([ - [7, false], - [null, false], - ['', false], - ['a', true], - ['AZ09', true], - ['a'.repeat(63), true], - ['a'.repeat(64), false], - ['a/b', false], - ['a-b', true], - ['-a-b', false], - ['a-b-', false], - ['a--b', true], - ['a_b', true], - ['a.b', true], - ['a..b', true], - ['a:b', true], - ])(`isValidIcon %p ? %p`, (value, matches) => { - expect(CommonValidatorFunctions.isValidIcon(value)).toBe(matches); - }); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 9a685094c8..295c5b8895 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -147,18 +147,4 @@ export class CommonValidatorFunctions { static isNonEmptyString(value: unknown): value is string { return typeof value === 'string' && value?.trim()?.length >= 1; } - - /** - * Checks that the value is a valid icon. - * - * @param value - The value to check - */ - static isValidIcon(value: unknown): boolean { - return ( - typeof value === 'string' && - value.length >= 1 && - value.length <= 63 && - /^([A-Za-z0-9][-A-Za-z0-9_.:]*)?[A-Za-z0-9]$/.test(value) - ); - } } From fb48a5a9d3bfbd107a2cffafb4b8909ac19f2ae2 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 16 May 2025 13:55:35 +0200 Subject: [PATCH 5/5] chore(catalog-model): use isNonEmptyString for icon validation Signed-off-by: Benjamin Janssens --- .../src/entity/policies/FieldFormatEntityPolicy.test.ts | 8 +++----- .../src/entity/policies/FieldFormatEntityPolicy.ts | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 9fe041a839..3efef8691a 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -197,7 +197,7 @@ describe('FieldFormatEntityPolicy', () => { }, ); - it.each([[123], [{}], [[]], ['abc xyz']])( + it.each([[123], [{}], [[]]])( 'rejects bad link icon value %s', async (icon: unknown) => { data.metadata.links = [{ url: 'http://foo', icon }]; @@ -208,10 +208,8 @@ describe('FieldFormatEntityPolicy', () => { it('rejects a single bad link icon value', async () => { data.metadata.links = [ { url: 'http://foo', icon: 'good' }, - { url: 'http://foo', icon: 'not good' }, + { url: 'http://foo', icon: 123 }, ]; - await expect(policy.enforce(data)).rejects.toThrow( - /links.1.icon.*"not good"/i, - ); + await expect(policy.enforce(data)).rejects.toThrow(/links.1.icon.*"123"/i); }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 18e9523478..961eb16ecd 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -161,7 +161,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { optional( `links.${i}.icon`, links[i]?.icon, - KubernetesValidatorFunctions.isValidObjectName, + CommonValidatorFunctions.isNonEmptyString, ); }