From 86eac5b4583c3e2e614865c8a2ead7568fe513c1 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 18 Aug 2020 18:35:45 +0200 Subject: [PATCH] feat(catalog): add tags to entity metadata This add a field tags to the entity metadata to specifiy a user defined classifications. Closes #1997 --- .../software-catalog/descriptor-format.md | 17 +++++++++++++++++ packages/catalog-model/src/entity/Entity.ts | 6 ++++++ .../policies/FieldFormatEntityPolicy.test.ts | 8 ++++++++ .../entity/policies/FieldFormatEntityPolicy.ts | 6 ++++++ .../policies/ReservedFieldsEntityPolicy.test.ts | 3 +++ .../policies/ReservedFieldsEntityPolicy.ts | 1 + .../policies/SchemaValidEntityPolicy.test.ts | 8 ++++++++ .../entity/policies/SchemaValidEntityPolicy.ts | 1 + .../src/validation/makeValidator.ts | 1 + packages/catalog-model/src/validation/types.ts | 1 + 10 files changed, 52 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 4751bd88f7..a7d57890dc 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -42,6 +42,7 @@ software catalog API. "labels": { "system": "public-websites" }, + "tags": ["java"], "name": "artist-web", "uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2" }, @@ -66,6 +67,8 @@ metadata: annotations: example.com/service-discovery: artistweb circleci.com/project-slug: gh/example-org/artist-website + tags: + - java spec: type: website lifecycle: production @@ -232,6 +235,20 @@ The `backstage.io/` prefix is reserved for use by Backstage core components. Values can be of any length, but are limited to being strings. +### `tags` [optional] + +A list of single-valued strings, for example to classify catalog entities in +various ways. This is different to the labels in metadata, as labels are +key-value pairs. + +The values are user defined, for example the programming language used for the +component, like `java` or `go`. + +This field is optional, and currently has no special semantics. + +Each tag must be sequences of `[a-zA-Z0-9]` separated by `-`, at most 63 +characters in total. + ## Kind: Component Describes the following entity kind: diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 4f245da8b7..7f323043b1 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -113,6 +113,12 @@ export type EntityMeta = JsonObject & { * entity. */ annotations?: Record; + + /** + * A list of single-valued strings, to for example classify catalog entities in + * various ways. + */ + tags?: string[]; }; /** diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 80cf70fbab..edf04a64cc 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -35,6 +35,9 @@ describe('FieldFormatEntityPolicy', () => { backstage.io/custom: ValueStuff annotations: example.com/bindings: are-secret + tags: + - java + - data-service spec: custom: stuff `); @@ -102,4 +105,9 @@ describe('FieldFormatEntityPolicy', () => { data.metadata.annotations.a = 7; await expect(policy.enforce(data)).rejects.toThrow(/annotation.*7/i); }); + + it('rejects bad tag value', async () => { + data.metadata.tags.push('Hello World'); + await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i); + }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 4ccd8ec711..20a3724079 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -83,6 +83,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy { require(`annotations.${k}`, v, this.validators.isValidAnnotationValue); } + const tags = entity.metadata.tags ?? []; + + for (let i = 0; i < tags.length; ++i) { + require(`tags.${i}`, tags[i], this.validators.isValidTag); + } + return entity; } } diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts index 04acd58686..5d4e60a1a3 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -35,6 +35,9 @@ describe('ReservedFieldsEntityPolicy', () => { backstage.io/custom: ValueStuff annotations: example.com/bindings: are-secret + tags: + - java + - data spec: custom: stuff `); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts index bbe374e811..edb63cf05c 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -29,6 +29,7 @@ const DEFAULT_RESERVED_ENTITY_FIELDS: string[] = [ 'metadata.description', 'metadata.labels', 'metadata.annotations', + 'metadata.tags', // The below items are known to appear in core kinds, and therefore should // not be appearing in metadata (which would indicate that the user made a // mistake in where to place them). diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index 2113772af4..d63ba49dbe 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -36,6 +36,9 @@ describe('SchemaValidEntityPolicy', () => { backstage.io/custom: ValueStuff annotations: example.com/bindings: are-secret + tags: + - java + - data spec: custom: stuff `); @@ -190,6 +193,11 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/annotations/); }); + it('rejects bad tags type', async () => { + data.metadata.tags = 7; + await expect(policy.enforce(data)).rejects.toThrow(/tags/); + }); + // // spec // diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index e901e82d8b..a6d376b4ac 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -31,6 +31,7 @@ const DEFAULT_ENTITY_SCHEMA = yup.object({ description: yup.string().notRequired(), labels: yup.object>().notRequired(), annotations: yup.object>().notRequired(), + tags: yup.array().notRequired(), }) .required(), spec: yup.object({}).notRequired(), diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index 7ca01365e0..3602c01a63 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -28,6 +28,7 @@ const defaultValidators: Validators = { isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, + isValidTag: CommonValidatorFunctions.isValidDnsLabel, }; export function makeValidator(overrides: Partial = {}): Validators { diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index 81209bfb75..ff00036991 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -24,4 +24,5 @@ export type Validators = { isValidLabelValue(value: any): boolean; isValidAnnotationKey(value: any): boolean; isValidAnnotationValue(value: any): boolean; + isValidTag(value: any): boolean; };