From b07e8af867ae91482c7f800b9b971b3d4255e822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 9 Jun 2020 11:49:13 +0200 Subject: [PATCH 1/6] Add description, owner, and lifecycle to the catalog mode --- packages/catalog-model/src/entity/Entity.ts | 6 ++ .../ReservedFieldsEntityPolicy.test.ts | 5 + .../policies/ReservedFieldsEntityPolicy.ts | 71 +++++++----- .../policies/SchemaValidEntityPolicy.test.ts | 30 ++++++ .../policies/SchemaValidEntityPolicy.ts | 28 +---- .../src/kinds/ComponentEntityV1beta1.test.ts | 101 ++++++++++++++++++ .../src/kinds/ComponentEntityV1beta1.ts | 6 +- 7 files changed, 198 insertions(+), 49 deletions(-) create mode 100644 packages/catalog-model/src/kinds/ComponentEntityV1beta1.test.ts diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 80c5765ee1..70d2ba4d2b 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -95,6 +95,12 @@ export type EntityMeta = { */ namespace?: string; + /** + * A short (typically relatively few words, on one line) description of the + * entity. + */ + description?: string; + /** * Key/value pairs of identifying information attached to the entity. */ diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts index 8db33955a7..5bc39f4e93 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -61,4 +61,9 @@ describe('ReservedFieldsEntityPolicy', () => { /annotation.*apiVersion/i, ); }); + + it('rejects core fields mistakenly placed in metadata', async () => { + data.metadata.owner = 'emma'; + await expect(policy.enforce(data)).rejects.toThrow(/owner/i); + }); }); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts index 57029ffa24..a258e9a16a 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -17,47 +17,70 @@ import { EntityPolicy } from '../../types'; import { Entity } from '../Entity'; -const DEFAULT_RESERVED_ENTITY_FIELDS = [ - 'apiVersion', - 'kind', - 'uid', - 'etag', - 'generation', - 'name', - 'namespace', - 'labels', - 'annotations', - 'spec', -]; +type ExceptWhere = 'metadata' | 'spec' | 'labels' | 'annotations'; +type ReservedFields = Record; + +const DEFAULT_RESERVED_ENTITY_FIELDS: ReservedFields = { + apiVersion: [], + kind: [], + spec: [], + uid: ['metadata'], + etag: ['metadata'], + generation: ['metadata'], + name: ['metadata'], + namespace: ['metadata'], + description: ['metadata'], + labels: ['metadata'], + annotations: ['metadata'], + // 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). + lifecycle: ['spec'], + owner: ['spec'], +}; /** * Ensures that fields are not given certain reserved names. */ export class ReservedFieldsEntityPolicy implements EntityPolicy { - private readonly reservedFields: string[]; + private readonly reservedFields: ReservedFields; - constructor(fields?: string[]) { - this.reservedFields = [ - ...(fields ?? []), + constructor(fields?: ReservedFields) { + this.reservedFields = { + ...(fields ?? {}), ...DEFAULT_RESERVED_ENTITY_FIELDS, - ]; + }; } async enforce(entity: Entity): Promise { - for (const field of this.reservedFields) { - if (entity.spec?.hasOwnProperty(field)) { + for (const [name, exceptWhere] of Object.entries(this.reservedFields)) { + if ( + !exceptWhere.includes('metadata') && + entity.metadata.hasOwnProperty(name) + ) { throw new Error( - `The spec may not contain the field ${field}, because it has reserved meaning`, + `The metadata may not contain the field ${name}, because it has reserved meaning`, ); } - if (entity.metadata.labels?.hasOwnProperty(field)) { + if (!exceptWhere.includes('spec') && entity.spec?.hasOwnProperty(name)) { throw new Error( - `A label may not have the field ${field}, because it has reserved meaning`, + `The spec may not contain the field ${name}, because it has reserved meaning`, ); } - if (entity.metadata.annotations?.hasOwnProperty(field)) { + if ( + !exceptWhere.includes('labels') && + entity.metadata.labels?.hasOwnProperty(name) + ) { throw new Error( - `An annotation may not have the field ${field}, because it has reserved meaning`, + `A label may not have the field ${name}, because it has reserved meaning`, + ); + } + if ( + !exceptWhere.includes('annotations') && + entity.metadata.annotations?.hasOwnProperty(name) + ) { + throw new Error( + `An annotation may not have the field ${name}, because it has reserved meaning`, ); } } diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index c53fbf3e46..9a3c02d85a 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -100,6 +100,11 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/uid/); }); + it('rejects empty uid', async () => { + data.metadata.uid = ''; + await expect(policy.enforce(data)).rejects.toThrow(/uid/); + }); + it('accepts missing etag', async () => { delete data.metadata.etag; await expect(policy.enforce(data)).resolves.toBe(data); @@ -110,6 +115,11 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/etag/); }); + it('rejects empty etag', async () => { + data.metadata.etag = ''; + await expect(policy.enforce(data)).rejects.toThrow(/etag/); + }); + it('accepts missing generation', async () => { delete data.metadata.generation; await expect(policy.enforce(data)).resolves.toBe(data); @@ -120,6 +130,16 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/generation/); }); + it('rejects zero generation', async () => { + data.metadata.generation = 0; + await expect(policy.enforce(data)).rejects.toThrow(/generation/); + }); + + it('rejects non-integer generation', async () => { + data.metadata.generation = 1.5; + await expect(policy.enforce(data)).rejects.toThrow(/generation/); + }); + it('rejects missing name', async () => { delete data.metadata.name; await expect(policy.enforce(data)).rejects.toThrow(/name/); @@ -140,6 +160,16 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/namespace/); }); + it('accepts missing description', async () => { + delete data.metadata.description; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it('rejects bad description type', async () => { + data.metadata.description = 7; + await expect(policy.enforce(data)).rejects.toThrow(/description/); + }); + it('accepts missing labels', async () => { delete data.metadata.labels; await expect(policy.enforce(data)).resolves.toBe(data); diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index d367022ff1..e901e82d8b 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -23,32 +23,12 @@ const DEFAULT_ENTITY_SCHEMA = yup.object({ kind: yup.string().required(), metadata: yup .object({ - uid: yup - .string() - .notRequired() - .test( - 'metadata.uid', - 'The uid must not be empty', - value => value === undefined || value.length > 0, - ), - etag: yup - .string() - .notRequired() - .test( - 'metadata.etag', - 'The etag must not be empty', - value => value === undefined || value.length > 0, - ), - generation: yup - .number() - .notRequired() - .test( - 'metadata.generation', - 'The generation must be an integer greater than zero', - value => value === undefined || (value === (value | 0) && value > 0), - ), + uid: yup.string().notRequired().min(1), + etag: yup.string().notRequired().min(1), + generation: yup.number().notRequired().integer().min(1), name: yup.string().required(), namespace: yup.string().notRequired(), + description: yup.string().notRequired(), labels: yup.object>().notRequired(), annotations: yup.object>().notRequired(), }) diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1beta1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1beta1.test.ts new file mode 100644 index 0000000000..a7988047b7 --- /dev/null +++ b/packages/catalog-model/src/kinds/ComponentEntityV1beta1.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPolicy } from '../types'; +import { + ComponentEntityV1beta1, + ComponentEntityV1beta1Policy, +} from './ComponentEntityV1beta1'; + +describe('ComponentV1beta1Policy', () => { + let entity: ComponentEntityV1beta1; + let policy: EntityPolicy; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test', + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'me', + }, + }; + policy = new ComponentEntityV1beta1Policy(); + }); + + it('happy path: accepts valid data', async () => { + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta2'; + await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/); + }); + + it('rejects unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(policy.enforce(entity)).rejects.toThrow(/kind/); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty wrong type', async () => { + (entity as any).spec.type = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects missing lifecycle', async () => { + delete (entity as any).spec.lifecycle; + await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects wrong lifecycle', async () => { + (entity as any).spec.lifecycle = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects wrong empty', async () => { + (entity as any).spec.lifecycle = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); + }); + + it('rejects missing owner', async () => { + delete (entity as any).spec.owner; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects wrong owner', async () => { + (entity as any).spec.owner = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects empty owner', async () => { + (entity as any).spec.owner = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); +}); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1beta1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1beta1.ts index 2f5e8e079c..02bcffbd28 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1beta1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1beta1.ts @@ -26,6 +26,8 @@ export interface ComponentEntityV1beta1 extends Entity { kind: typeof KIND; spec: { type: string; + lifecycle: string; + owner: string; }; } @@ -36,7 +38,9 @@ export class ComponentEntityV1beta1Policy implements EntityPolicy { this.schema = yup.object>({ spec: yup .object({ - type: yup.string().required(), + type: yup.string().required().min(1), + lifecycle: yup.string().required().min(1), + owner: yup.string().required().min(1), }) .required(), }); From 15ebc3cf15c362a9fbd9a13a94211a97971bdd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 9 Jun 2020 14:15:48 +0200 Subject: [PATCH 2/6] Add documentation --- .../software-catalog/descriptor-format.md | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 docs/features/software-catalog/descriptor-format.md diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md new file mode 100644 index 0000000000..f3999e1eec --- /dev/null +++ b/docs/features/software-catalog/descriptor-format.md @@ -0,0 +1,295 @@ +# Descriptor Format of Catalog Entities + +This section describes the default data shape and semantics of catalog entities. + +This both applies to objects given to and returned from the software catalog API, +as well as to the descriptor files that the software catalog can ingest natively. In +the API request/response cycle, a JSON representation is used, while the descriptor +files are on YAML format to be more easily maintainable by humans. However, the +structure and semantics is the same in both cases. + +## Contents + +- [Overall Shape Of An Entity](#overall-shape-of-an-entity) +- [Common to All Kinds: The Envelope](#common-to-all-kinds-the-envelope) +- [Common to All Kinds: The Metadata](#common-to-all-kinds-the-metadata) +- [Kind: Component](#kind-component) + +## Overall Shape Of An Entity + +The following is an example of the shape of an entity as returned from the software +catalog API. + +```js +{ + "apiVersion": "backstage.io/v1beta1", + "kind": "Component", + "metadata": { + "annotations": { + "backstage.io/managed-by-location": "file:/tmp/component-info.yaml", + "example.com/service-discovery": "artistweb", + "circleci.com/project-slug": "gh/example-org/artist-website" + }, + "description": "The place to be, for great artists", + "etag": "ZjU2MWRkZWUtMmMxZS00YTZiLWFmMWMtOTE1NGNiZDdlYzNk", + "generation": 1, + "labels": { + "system": "public-websites" + }, + "name": "artist-web", + "uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2" + }, + "spec": { + "lifecycle": "production", + "owner": "artist-relations@example.com", + "type": "website" + } +} +``` + +The corresponding descriptor file that generated it may look as follows: + +```yaml +apiVersion: backstage.io/v1beta1 +kind: Component +metadata: + name: artist-web + description: The place to be, for great artists + labels: + system: public-websites + annotations: + example.com/service-discovery: artistweb + circleci.com/project-slug: gh/example-org/artist-website +spec: + type: website + lifecycle: production + owner: artist-relations@example.com +``` + +The root fields `apiVersion`, `kind`, `metadata`, and `spec` are part of the +_envelope_, defining the overall structure of all kinds of entity. Likewise, +some metadata fields like `name`, `labels`, and `annotations` are of +special significance and have reserved purposes and distinct shapes. + +See below for details about these fields. + +## Common to All Kinds: The Envelope + +The root envelope object has the following structure. + +### `apiVersion` and `kind` [required] + +The `kind` is the high level entity type being described. The +[Backstage system model](https://github.com/spotify/backstage/issues/390) describes +a number of core kinds that plugins can know of and understand, but an organization +using Backstage is free to also add entities of other kinds to the catalog. + +The `apiVersion` is the version of specification format for that particular +entity that the specification is made against. The version is used for being able to +evolve the format, and the tuple of `apiVersion` and `kind` should be enough +for a parser to know how to interpret the contents of the rest of the data. + +Backstage specific entities have an `apiVersion` that is prefixed with +`backstage.io/`, to distinguish them from other types of object that share +the same type of structure. This may be relevant when co-hosting these +specifications with e.g. kubernetes object manifests, or when an organization +adds their own specific kinds of entity to the catalog. + +Early versions of the catalog will be using beta versions, e.g. `backstage.io/v1beta1`, +to signal that the format may still change. After that, we will be using +`backstage.io/v1` and up. + +### `metadata` [required] + +A structure that contains metadata about the entity, i.e. things that aren't directly +part of the entity specification itself. See below for more details about this structure. + +### `spec` [varies] + +The actual specification data that describes the entity. + +The precise structure of the `spec` depends on the `apiVersion` and `kind` combination, +and some kinds may not even have a `spec` at all. See further down in this document for +the specification structure of specific kinds. + +## Common to All Kinds: The Metadata + +The `metadata` root field has a number of reserved fields with specific meaning, described +below. + +In addition to these, you may add any number of other fields directly under `metadata`, but +be aware that general plugins and tools may not be able to understand their semantics. + +### `name` [required] + +The name of the entity. This name is both meant for human eyes to recognize the entity, +and for machines and other components to reference the entity (e.g. in URLs or from +other entity specification files). + +Names must be unique per kind, within a given namespace (if specified), at any point in +time. Names may be reused at a later time, after an entity is deleted from the registry. + +Names are required to follow a certain format. Entities that do not follow those rules +will not be accepted for registration in the catalog. The ruleset is configurable to fit +your organization's needs, but the default behavior is as follows. + +- Strings of length at least 1, and at most 63 +- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of `[-_.]` + +Example: `visits-tracking-service`, `CircleciBuildsDump_avro_gcs` + +In addition to this, names are passed through a normalization function and then compared +to the same normalized form of other entity names and made sure to not collide. This rule +of uniqueness exists to avoid situations where e.g. both `my-component` and `MyComponent` +are registered side by side, which leads to confusion and risk. The normalization function +is also configurable, but the default behavior is as follows. + +- Strip out all characters outside of the set `[a-zA-Z0-9]` +- Convert to lowercase + +Example: `CircleciBuildsDs_avro_gcs` -> `circlecibuildsdsavrogcs` + +### `namespace` [optional] + +The ID of a namespace that the entity belongs to. This is a string that follows the same +format restrictions as `name` above. + +This field is optional, and currently has no special semantics apart from bounding the +name uniqueness constraint if specified. It is reserved for future use and may get broader +semantic implication later. For now, it is recommended to not specify a namespace unless +you have specific need to do so. + +Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, +i.e. not Backstage specific but the same as in Kubernetes. + +### `description` [optional] + +A human readable description of the entity, to be shown in Backstage. Should be kept short +and informative, suitable to give an overview of the entity's purpose at a glance. More +detailed explanations and documentation should be placed elsewhere. + +### `labels` [optional] + +Labels are optional key/value pairs of that are attached to the entity, and their use is +identical to [Kubernetes object labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/). + +Their main purpose is for references to other entities, and for information that is +in one way or another classifying for the current entity. They are often used as values +in queries or filters. + +Both the key and the value are strings, subject to the following restrictions. + +Keys have an optional prefix followed by a slash, and then the name part which is required. +The prefix, if present, must be a valid lowercase domain name, at most 253 characters in total. +The name part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters +in total. + +The `backstage.io/` prefix is reserved for use by Backstage core components. Some keys such as +`system` also have predefined semantics. + +Values are strings that follow the same restrictions as `name` above. + +### `annotations` [optional] + +An object with arbitrary non-identifying metadata attached to the entity, +identical in use to [Kubernetes object annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/). + +Their purpose is mainly, but not limited, to reference into external systems. This could +for example be a reference to the git ref the entity was ingested from, to monitoring +and logging systems, to pagerduty schedules, etc. Users may add these to descriptor +YAML files, but in addition to this automated systems may also add annotations, either +during ingestion into the catalog, or at a later time. + +Both the key and the value are strings, subject to the following restrictions. + +Keys have an optional prefix followed by a slash, and then the name part which is required. +The prefix must be a valid lowercase domain name if specified, at most 253 characters in total. +The name part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters +in total. + +The `backstage.io/` prefix is reserved for use by Backstage core components. + +Values can be of any length, but are limited to being strings. + +## Kind: Component + +Describes the following entity kind: + +| Field | Value | +| ------------ | ---------------------- | +| `apiVersion` | `backstage.io/v1beta1` | +| `kind` | `Component` | + +A Component describes a software component. It is typically intimately linked to the source code +that constitutes the component, and should be what a developer may regard a "unit of software", +usually with a distinct deployable or linkable artifact. + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1beta1 +kind: Component +metadata: + name: artist-web + description: The place to be, for great artists +spec: + type: website + lifecycle: production + owner: artist-relations@example.com +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) shape, this kind has the +following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1beta1` and `Component`, respectively. + +### `spec.type` [required] + +The type of component as a string, e.g. `website`. This field is required. + +The software catalog accepts any type value, but an organisation should take great +care to establish a proper taxonomy for these. Tools including Backstage itself may +read this field and behave differently depending on its value. For example, a website +type component may present tooling in the Backstage interface that is specific to just +websites. + +The current set of well-known and common values for this field is: + +- `service` - a backend service, typically exposing an API +- `website` - a website +- `library` - a software library, such as an NPM module or a Java library + +### `spec.lifecycle` [required] + +The lifecyle state of the component, e.g. `production`. This field is required. + +The software catalog accepts any lifecycle value, but an organisation should take great +care to establish a proper taxonomy for these. + +The current set of well-known and common values for this field is: + +- `experimental` - an experiment or early, non-production component, signaling that users + may not prefer to consume it over other more established components, or that there are + low or no reliability guarantees +- `production` - an established, owned, maintained component +- `deprecated` - a component that is at the end of its lifecycle, and may disappear at a + later point in time + +### `spec.owner` [required] + +The owner of the component, e.g. `artist-relations@example.com`. This field is required. + +In Backstage, the owner of a component is the singular entity (commonly a team) that +bears ultimate responsibility for the component, and has the authority and capability +to develop and maintain it. They will be the point of contact if something goes wrong, +or if features are to be requested. The main purpose of this field is for display +purposes in Backstage, so that people looking at catalog items can get an understanding +of to whom this component belongs. It is not to be used by automated processes to for +example assign authorization in runtime systems. There may be others that also develop +or otherwise touch the component, but there will always be one ultimate owner. + +Apart from being a string, the software catalog leaves the format of this field open to +implementers to choose. Most commonly, it is set to the ID or email of a group of people +in an organizational structure. From 8628235562267b6db0ffdcc8011dd9691e797c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 9 Jun 2020 15:00:53 +0200 Subject: [PATCH 3/6] Make DEFAULT_RESERVED_ENTITY_FIELDS list of strings again --- .../policies/ReservedFieldsEntityPolicy.ts | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts index a258e9a16a..bbe374e811 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -17,66 +17,61 @@ import { EntityPolicy } from '../../types'; import { Entity } from '../Entity'; -type ExceptWhere = 'metadata' | 'spec' | 'labels' | 'annotations'; -type ReservedFields = Record; - -const DEFAULT_RESERVED_ENTITY_FIELDS: ReservedFields = { - apiVersion: [], - kind: [], - spec: [], - uid: ['metadata'], - etag: ['metadata'], - generation: ['metadata'], - name: ['metadata'], - namespace: ['metadata'], - description: ['metadata'], - labels: ['metadata'], - annotations: ['metadata'], +const DEFAULT_RESERVED_ENTITY_FIELDS: string[] = [ + 'apiVersion', + 'kind', + 'spec', + 'metadata.uid', + 'metadata.etag', + 'metadata.generation', + 'metadata.name', + 'metadata.namespace', + 'metadata.description', + 'metadata.labels', + 'metadata.annotations', // 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). - lifecycle: ['spec'], - owner: ['spec'], -}; + 'spec.lifecycle', + 'spec.owner', +]; /** * Ensures that fields are not given certain reserved names. */ export class ReservedFieldsEntityPolicy implements EntityPolicy { - private readonly reservedFields: ReservedFields; + private readonly reservedFields: string[]; - constructor(fields?: ReservedFields) { - this.reservedFields = { - ...(fields ?? {}), + constructor(fields?: string[]) { + this.reservedFields = [ + ...(fields ?? []), ...DEFAULT_RESERVED_ENTITY_FIELDS, - }; + ]; } async enforce(entity: Entity): Promise { - for (const [name, exceptWhere] of Object.entries(this.reservedFields)) { - if ( - !exceptWhere.includes('metadata') && - entity.metadata.hasOwnProperty(name) - ) { + for (const path of this.reservedFields) { + const [where, name] = path.includes('.') + ? path.split('.') + : [undefined, path]; + + if (where !== 'metadata' && entity.metadata.hasOwnProperty(name)) { throw new Error( `The metadata may not contain the field ${name}, because it has reserved meaning`, ); } - if (!exceptWhere.includes('spec') && entity.spec?.hasOwnProperty(name)) { + if (where !== 'spec' && entity.spec?.hasOwnProperty(name)) { throw new Error( `The spec may not contain the field ${name}, because it has reserved meaning`, ); } - if ( - !exceptWhere.includes('labels') && - entity.metadata.labels?.hasOwnProperty(name) - ) { + if (where !== 'labels' && entity.metadata.labels?.hasOwnProperty(name)) { throw new Error( `A label may not have the field ${name}, because it has reserved meaning`, ); } if ( - !exceptWhere.includes('annotations') && + where !== 'annotations' && entity.metadata.annotations?.hasOwnProperty(name) ) { throw new Error( From 8b1c795840847457d7b1c51f1032d65a34d663ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 10 Jun 2020 10:42:47 +0200 Subject: [PATCH 4/6] Point to ADR instead of issue for catalog kinds --- docs/features/software-catalog/descriptor-format.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index f3999e1eec..96707d12df 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -79,11 +79,14 @@ The root envelope object has the following structure. ### `apiVersion` and `kind` [required] -The `kind` is the high level entity type being described. The -[Backstage system model](https://github.com/spotify/backstage/issues/390) describes +The `kind` is the high level entity type being described. +[ADR005](/docs/architecture-decisions/adr005-catalog-core-entities.md) describes a number of core kinds that plugins can know of and understand, but an organization using Backstage is free to also add entities of other kinds to the catalog. +The perhaps most central kind of entity, that the catalog focuses on in the initial +phase, is `Component` ([see below](#kind-component)). + The `apiVersion` is the version of specification format for that particular entity that the specification is made against. The version is used for being able to evolve the format, and the tuple of `apiVersion` and `kind` should be enough From e01fae0a28ea9d069f010a0a738e4587ffd4bb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Jun 2020 13:00:31 +0200 Subject: [PATCH 5/6] use alpha versioning in the catalog instead --- packages/catalog-model/src/EntityPolicies.ts | 8 +- ...est.ts => ComponentEntityV1alpha1.test.ts} | 25 +++-- ...yV1beta1.ts => ComponentEntityV1alpha1.ts} | 18 ++- .../src/kinds/LocationEntityV1alpha1.test.ts | 103 ++++++++++++++++++ ...tyV1beta1.ts => LocationEntityV1alpha1.ts} | 22 ++-- packages/catalog-model/src/kinds/index.ts | 16 +-- .../examples/artist-lookup-component.yaml | 2 +- .../examples/playback-order-component.yaml | 2 +- .../examples/podcast-api-component.yaml | 2 +- .../examples/queue-proxy-component.yaml | 2 +- .../examples/searcher-component.yaml | 2 +- .../examples/shuffle-api-component.yaml | 2 +- .../src/catalog/DatabaseEntitiesCatalog.ts | 5 +- 13 files changed, 156 insertions(+), 53 deletions(-) rename packages/catalog-model/src/kinds/{ComponentEntityV1beta1.test.ts => ComponentEntityV1alpha1.test.ts} (81%) rename packages/catalog-model/src/kinds/{ComponentEntityV1beta1.ts => ComponentEntityV1alpha1.ts} (72%) create mode 100644 packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts rename packages/catalog-model/src/kinds/{LocationEntityV1beta1.ts => LocationEntityV1alpha1.ts} (67%) diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index cf23bcdc88..d761aa9f20 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -23,8 +23,8 @@ import { SchemaValidEntityPolicy, } from './entity'; import { - ComponentEntityV1beta1Policy, - LocationEntityV1beta1Policy, + ComponentEntityV1alpha1Policy, + LocationEntityV1alpha1Policy, } from './kinds'; import { EntityPolicy } from './types'; @@ -72,8 +72,8 @@ export class EntityPolicies implements EntityPolicy { new ReservedFieldsEntityPolicy(), ]), EntityPolicies.anyOf([ - new ComponentEntityV1beta1Policy(), - new LocationEntityV1beta1Policy(), + new ComponentEntityV1alpha1Policy(), + new LocationEntityV1alpha1Policy(), ]), ]); } diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1beta1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts similarity index 81% rename from packages/catalog-model/src/kinds/ComponentEntityV1beta1.test.ts rename to packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts index a7988047b7..f1a4b3c2c0 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1beta1.test.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts @@ -16,17 +16,17 @@ import { EntityPolicy } from '../types'; import { - ComponentEntityV1beta1, - ComponentEntityV1beta1Policy, -} from './ComponentEntityV1beta1'; + ComponentEntityV1alpha1, + ComponentEntityV1alpha1Policy, +} from './ComponentEntityV1alpha1'; -describe('ComponentV1beta1Policy', () => { - let entity: ComponentEntityV1beta1; +describe('ComponentV1alpha1Policy', () => { + let entity: ComponentEntityV1alpha1; let policy: EntityPolicy; beforeEach(() => { entity = { - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'test', @@ -37,15 +37,20 @@ describe('ComponentV1beta1Policy', () => { owner: 'me', }, }; - policy = new ComponentEntityV1beta1Policy(); + policy = new ComponentEntityV1alpha1Policy(); }); it('happy path: accepts valid data', async () => { await expect(policy.enforce(entity)).resolves.toBe(entity); }); + it('happy path: silently accepts v1beta1 as well', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta1'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + it('rejects unknown apiVersion', async () => { - (entity as any).apiVersion = 'backstage.io/v1beta2'; + (entity as any).apiVersion = 'backstage.io/v1beta0'; await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/); }); @@ -64,7 +69,7 @@ describe('ComponentV1beta1Policy', () => { await expect(policy.enforce(entity)).rejects.toThrow(/type/); }); - it('rejects empty wrong type', async () => { + it('rejects empty type', async () => { (entity as any).spec.type = ''; await expect(policy.enforce(entity)).rejects.toThrow(/type/); }); @@ -79,7 +84,7 @@ describe('ComponentV1beta1Policy', () => { await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); }); - it('rejects wrong empty', async () => { + it('rejects empty lifecycle', async () => { (entity as any).spec.lifecycle = ''; await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/); }); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1beta1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts similarity index 72% rename from packages/catalog-model/src/kinds/ComponentEntityV1beta1.ts rename to packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 02bcffbd28..68cf0c61bb 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1beta1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -18,11 +18,11 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; import type { EntityPolicy } from '../types'; -const API_VERSION = 'backstage.io/v1beta1'; -const KIND = 'Component'; +const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; +const KIND = 'Component' as const; -export interface ComponentEntityV1beta1 extends Entity { - apiVersion: typeof API_VERSION; +export interface ComponentEntityV1alpha1 extends Entity { + apiVersion: typeof API_VERSION[number]; kind: typeof KIND; spec: { type: string; @@ -31,11 +31,13 @@ export interface ComponentEntityV1beta1 extends Entity { }; } -export class ComponentEntityV1beta1Policy implements EntityPolicy { +export class ComponentEntityV1alpha1Policy implements EntityPolicy { private schema: yup.Schema; constructor() { - this.schema = yup.object>({ + this.schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), spec: yup .object({ type: yup.string().required().min(1), @@ -47,10 +49,6 @@ export class ComponentEntityV1beta1Policy implements EntityPolicy { } async enforce(envelope: Entity): Promise { - if (envelope.apiVersion !== API_VERSION || envelope.kind !== KIND) { - throw new Error('Unsupported apiVersion / kind'); - } - return await this.schema.validate(envelope, { strict: true }); } } diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts new file mode 100644 index 0000000000..e3c8170af2 --- /dev/null +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityPolicy } from '../types'; +import { + LocationEntityV1alpha1, + LocationEntityV1alpha1Policy, +} from './LocationEntityV1alpha1'; + +describe('LocationV1alpha1Policy', () => { + let entity: LocationEntityV1alpha1; + let policy: EntityPolicy; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'test', + }, + spec: { + type: 'github', + }, + }; + policy = new LocationEntityV1alpha1Policy(); + }); + + it('happy path: accepts valid data', async () => { + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('happy path: silently accepts v1beta1 as well', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta1'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/); + }); + + it('rejects unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(policy.enforce(entity)).rejects.toThrow(/kind/); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('accepts good target', async () => { + (entity as any).spec.target = + 'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/artist-lookup-component.yaml'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects wrong target', async () => { + (entity as any).spec.target = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/target/); + }); + + it('rejects empty target', async () => { + (entity as any).spec.target = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/target/); + }); + + it('accepts good targets', async () => { + (entity as any).spec.targets = [ + 'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/artist-lookup-component.yaml', + 'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/playback-order-component.yaml', + ]; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects wrong targets', async () => { + (entity as any).spec.targets = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/targets/); + }); +}); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1beta1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts similarity index 67% rename from packages/catalog-model/src/kinds/LocationEntityV1beta1.ts rename to packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index e68bb00aa0..a69a3a574a 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1beta1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -18,11 +18,11 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; import type { EntityPolicy } from '../types'; -const API_VERSION = 'backstage.io/v1beta1'; -const KIND = 'Location'; +const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; +const KIND = 'Location' as const; -export interface LocationEntityV1beta1 extends Entity { - apiVersion: typeof API_VERSION; +export interface LocationEntityV1alpha1 extends Entity { + apiVersion: typeof API_VERSION[number]; kind: typeof KIND; spec: { type: string; @@ -31,15 +31,17 @@ export interface LocationEntityV1beta1 extends Entity { }; } -export class LocationEntityV1beta1Policy implements EntityPolicy { +export class LocationEntityV1alpha1Policy implements EntityPolicy { private schema: yup.Schema; constructor() { - this.schema = yup.object>({ + this.schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), spec: yup .object({ - type: yup.string().required(), - target: yup.string().notRequired(), + type: yup.string().required().min(1), + target: yup.string().notRequired().min(1), targets: yup.array(yup.string()).notRequired(), }) .required(), @@ -47,10 +49,6 @@ export class LocationEntityV1beta1Policy implements EntityPolicy { } async enforce(envelope: Entity): Promise { - if (envelope.apiVersion !== API_VERSION || envelope.kind !== KIND) { - throw new Error('Unsupported apiVersion / kind'); - } - return await this.schema.validate(envelope, { strict: true }); } } diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 2d2a0744c5..85e22f7029 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -export { ComponentEntityV1beta1Policy } from './ComponentEntityV1beta1'; +export { ComponentEntityV1alpha1Policy } from './ComponentEntityV1alpha1'; export type { - ComponentEntityV1beta1 as ComponentEntity, - ComponentEntityV1beta1, -} from './ComponentEntityV1beta1'; -export { LocationEntityV1beta1Policy } from './LocationEntityV1beta1'; + ComponentEntityV1alpha1 as ComponentEntity, + ComponentEntityV1alpha1, +} from './ComponentEntityV1alpha1'; +export { LocationEntityV1alpha1Policy } from './LocationEntityV1alpha1'; export type { - LocationEntityV1beta1 as LocationEntity, - LocationEntityV1beta1, -} from './LocationEntityV1beta1'; + LocationEntityV1alpha1 as LocationEntity, + LocationEntityV1alpha1, +} from './LocationEntityV1alpha1'; diff --git a/plugins/catalog-backend/examples/artist-lookup-component.yaml b/plugins/catalog-backend/examples/artist-lookup-component.yaml index 5fbb073c1a..bb7d8d5767 100644 --- a/plugins/catalog-backend/examples/artist-lookup-component.yaml +++ b/plugins/catalog-backend/examples/artist-lookup-component.yaml @@ -1,4 +1,4 @@ -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: artist-lookup diff --git a/plugins/catalog-backend/examples/playback-order-component.yaml b/plugins/catalog-backend/examples/playback-order-component.yaml index 4ee959f48f..fa9f68e77a 100644 --- a/plugins/catalog-backend/examples/playback-order-component.yaml +++ b/plugins/catalog-backend/examples/playback-order-component.yaml @@ -1,4 +1,4 @@ -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: playback-order diff --git a/plugins/catalog-backend/examples/podcast-api-component.yaml b/plugins/catalog-backend/examples/podcast-api-component.yaml index c2c7ee34dd..2a27a0a2c5 100644 --- a/plugins/catalog-backend/examples/podcast-api-component.yaml +++ b/plugins/catalog-backend/examples/podcast-api-component.yaml @@ -1,4 +1,4 @@ -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: podcast-api diff --git a/plugins/catalog-backend/examples/queue-proxy-component.yaml b/plugins/catalog-backend/examples/queue-proxy-component.yaml index 8298b19f93..a9f41b8776 100644 --- a/plugins/catalog-backend/examples/queue-proxy-component.yaml +++ b/plugins/catalog-backend/examples/queue-proxy-component.yaml @@ -1,4 +1,4 @@ -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: queue-proxy diff --git a/plugins/catalog-backend/examples/searcher-component.yaml b/plugins/catalog-backend/examples/searcher-component.yaml index ecfd92f470..d82f6e42c7 100644 --- a/plugins/catalog-backend/examples/searcher-component.yaml +++ b/plugins/catalog-backend/examples/searcher-component.yaml @@ -1,4 +1,4 @@ -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: searcher diff --git a/plugins/catalog-backend/examples/shuffle-api-component.yaml b/plugins/catalog-backend/examples/shuffle-api-component.yaml index d45a06e030..ed0517835c 100644 --- a/plugins/catalog-backend/examples/shuffle-api-component.yaml +++ b/plugins/catalog-backend/examples/shuffle-api-component.yaml @@ -1,4 +1,4 @@ -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: shuffle-api diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 6ab660a587..7c4b964b1b 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import type { Entity } from '@backstage/catalog-model'; -import { LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { NotFoundError } from '@backstage/backend-common'; - +import { LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import type { Entity } from '@backstage/catalog-model'; import type { Database, DbEntityResponse, EntityFilters } from '../database'; import type { EntitiesCatalog } from './types'; From 7d8c50bdacb80f8d6abd2f90031a8e42474ff862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Jun 2020 13:28:10 +0200 Subject: [PATCH 6/6] Remove remaining occurrences of v1beta1 --- .../adr002-default-catalog-file-format.md | 14 +- .../adr005-catalog-core-entities.md | 6 +- .../software-catalog/descriptor-format.md | 237 ++++++++++-------- .../DefaultNamespaceEntityPolicy.test.ts | 4 +- .../policies/FieldFormatEntityPolicy.test.ts | 2 +- .../NoForeignRootFieldsEntityPolicy.test.ts | 2 +- .../ReservedFieldsEntityPolicy.test.ts | 2 +- .../policies/SchemaValidEntityPolicy.test.ts | 2 +- .../src/kinds/ComponentEntityV1alpha1.test.ts | 2 +- .../src/kinds/LocationEntityV1alpha1.test.ts | 2 +- .../ingestion/HigherOrderOperations.test.ts | 4 +- .../CatalogPage/CatalogPage.test.tsx | 2 +- .../CatalogTable/CatalogTable.test.tsx | 6 +- .../EntityMetadataCard.test.tsx | 2 +- 14 files changed, 151 insertions(+), 136 deletions(-) diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index 6cf649aedf..00c8b0947d 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -52,7 +52,7 @@ This is an example entity definition with some mocked data. ```yaml --- -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: frobs-awesome @@ -96,8 +96,8 @@ Backstage specific entities have an `apiVersion` that is prefixed with same type of structure. This may be relevant when co-hosting these specifications with e.g. kubernetes object manifests. -Early versions of the catalog will be using beta versions, e.g. -`backstage.io/v1beta1`, to signal that the format may still change. After that, +Early versions of the catalog will be using alpha/beta versions, e.g. +`backstage.io/v1alpha1`, to signal that the format may still change. After that, we will be using `backstage.io/v1` and up. ### `metadata` @@ -213,10 +213,10 @@ Values can be of any length, but are limited to being strings. ## Component -| Field | Value | -| ------------ | ---------------------- | -| `apiVersion` | `backstage.io/v1beta1` | -| `kind` | `Component` | +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `Component` | The `spec` object for this kind is as follows: diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index 578eb72b91..d607dc62fb 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -37,7 +37,7 @@ Component entities are typically defined in YAML descriptor files next to the code of the component, and could look like this (actual schema will evolve): ```yaml -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: my-component-name @@ -63,7 +63,7 @@ wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): ```yaml -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: API metadata: name: my-component-api @@ -97,7 +97,7 @@ files, but would be stored in the catalog somewhat like this (actual schema will evolve): ```yaml -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Resource metadata: name: my-component-db diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 96707d12df..a9fc17ce75 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -2,11 +2,11 @@ This section describes the default data shape and semantics of catalog entities. -This both applies to objects given to and returned from the software catalog API, -as well as to the descriptor files that the software catalog can ingest natively. In -the API request/response cycle, a JSON representation is used, while the descriptor -files are on YAML format to be more easily maintainable by humans. However, the -structure and semantics is the same in both cases. +This both applies to objects given to and returned from the software catalog +API, as well as to the descriptor files that the software catalog can ingest +natively. In the API request/response cycle, a JSON representation is used, +while the descriptor files are on YAML format to be more easily maintainable by +humans. However, the structure and semantics is the same in both cases. ## Contents @@ -17,12 +17,12 @@ structure and semantics is the same in both cases. ## Overall Shape Of An Entity -The following is an example of the shape of an entity as returned from the software -catalog API. +The following is an example of the shape of an entity as returned from the +software catalog API. ```js { - "apiVersion": "backstage.io/v1beta1", + "apiVersion": "backstage.io/v1alpha1", "kind": "Component", "metadata": { "annotations": { @@ -50,7 +50,7 @@ catalog API. The corresponding descriptor file that generated it may look as follows: ```yaml -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: artist-web @@ -68,8 +68,8 @@ spec: The root fields `apiVersion`, `kind`, `metadata`, and `spec` are part of the _envelope_, defining the overall structure of all kinds of entity. Likewise, -some metadata fields like `name`, `labels`, and `annotations` are of -special significance and have reserved purposes and distinct shapes. +some metadata fields like `name`, `labels`, and `annotations` are of special +significance and have reserved purposes and distinct shapes. See below for details about these fields. @@ -81,71 +81,79 @@ The root envelope object has the following structure. The `kind` is the high level entity type being described. [ADR005](/docs/architecture-decisions/adr005-catalog-core-entities.md) describes -a number of core kinds that plugins can know of and understand, but an organization -using Backstage is free to also add entities of other kinds to the catalog. +a number of core kinds that plugins can know of and understand, but an +organization using Backstage is free to also add entities of other kinds to the +catalog. -The perhaps most central kind of entity, that the catalog focuses on in the initial -phase, is `Component` ([see below](#kind-component)). +The perhaps most central kind of entity, that the catalog focuses on in the +initial phase, is `Component` ([see below](#kind-component)). The `apiVersion` is the version of specification format for that particular -entity that the specification is made against. The version is used for being able to -evolve the format, and the tuple of `apiVersion` and `kind` should be enough -for a parser to know how to interpret the contents of the rest of the data. +entity that the specification is made against. The version is used for being +able to evolve the format, and the tuple of `apiVersion` and `kind` should be +enough for a parser to know how to interpret the contents of the rest of the +data. Backstage specific entities have an `apiVersion` that is prefixed with -`backstage.io/`, to distinguish them from other types of object that share -the same type of structure. This may be relevant when co-hosting these +`backstage.io/`, to distinguish them from other types of object that share the +same type of structure. This may be relevant when co-hosting these specifications with e.g. kubernetes object manifests, or when an organization adds their own specific kinds of entity to the catalog. -Early versions of the catalog will be using beta versions, e.g. `backstage.io/v1beta1`, -to signal that the format may still change. After that, we will be using -`backstage.io/v1` and up. +Early versions of the catalog will be using alpha/beta versions, e.g. +`backstage.io/v1alpha1`, to signal that the format may still change. After that, +we will be using `backstage.io/v1` and up. ### `metadata` [required] -A structure that contains metadata about the entity, i.e. things that aren't directly -part of the entity specification itself. See below for more details about this structure. +A structure that contains metadata about the entity, i.e. things that aren't +directly part of the entity specification itself. See below for more details +about this structure. ### `spec` [varies] The actual specification data that describes the entity. -The precise structure of the `spec` depends on the `apiVersion` and `kind` combination, -and some kinds may not even have a `spec` at all. See further down in this document for -the specification structure of specific kinds. +The precise structure of the `spec` depends on the `apiVersion` and `kind` +combination, and some kinds may not even have a `spec` at all. See further down +in this document for the specification structure of specific kinds. ## Common to All Kinds: The Metadata -The `metadata` root field has a number of reserved fields with specific meaning, described -below. +The `metadata` root field has a number of reserved fields with specific meaning, +described below. -In addition to these, you may add any number of other fields directly under `metadata`, but -be aware that general plugins and tools may not be able to understand their semantics. +In addition to these, you may add any number of other fields directly under +`metadata`, but be aware that general plugins and tools may not be able to +understand their semantics. ### `name` [required] -The name of the entity. This name is both meant for human eyes to recognize the entity, -and for machines and other components to reference the entity (e.g. in URLs or from -other entity specification files). +The name of the entity. This name is both meant for human eyes to recognize the +entity, and for machines and other components to reference the entity (e.g. in +URLs or from other entity specification files). -Names must be unique per kind, within a given namespace (if specified), at any point in -time. Names may be reused at a later time, after an entity is deleted from the registry. +Names must be unique per kind, within a given namespace (if specified), at any +point in time. Names may be reused at a later time, after an entity is deleted +from the registry. -Names are required to follow a certain format. Entities that do not follow those rules -will not be accepted for registration in the catalog. The ruleset is configurable to fit -your organization's needs, but the default behavior is as follows. +Names are required to follow a certain format. Entities that do not follow those +rules will not be accepted for registration in the catalog. The ruleset is +configurable to fit your organization's needs, but the default behavior is as +follows. - Strings of length at least 1, and at most 63 -- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of `[-_.]` +- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of + `[-_.]` Example: `visits-tracking-service`, `CircleciBuildsDump_avro_gcs` -In addition to this, names are passed through a normalization function and then compared -to the same normalized form of other entity names and made sure to not collide. This rule -of uniqueness exists to avoid situations where e.g. both `my-component` and `MyComponent` -are registered side by side, which leads to confusion and risk. The normalization function -is also configurable, but the default behavior is as follows. +In addition to this, names are passed through a normalization function and then +compared to the same normalized form of other entity names and made sure to not +collide. This rule of uniqueness exists to avoid situations where e.g. both +`my-component` and `MyComponent` are registered side by side, which leads to +confusion and risk. The normalization function is also configurable, but the +default behavior is as follows. - Strip out all characters outside of the set `[a-zA-Z0-9]` - Convert to lowercase @@ -154,61 +162,65 @@ Example: `CircleciBuildsDs_avro_gcs` -> `circlecibuildsdsavrogcs` ### `namespace` [optional] -The ID of a namespace that the entity belongs to. This is a string that follows the same -format restrictions as `name` above. +The ID of a namespace that the entity belongs to. This is a string that follows +the same format restrictions as `name` above. -This field is optional, and currently has no special semantics apart from bounding the -name uniqueness constraint if specified. It is reserved for future use and may get broader -semantic implication later. For now, it is recommended to not specify a namespace unless -you have specific need to do so. +This field is optional, and currently has no special semantics apart from +bounding the name uniqueness constraint if specified. It is reserved for future +use and may get broader semantic implication later. For now, it is recommended +to not specify a namespace unless you have specific need to do so. Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, i.e. not Backstage specific but the same as in Kubernetes. ### `description` [optional] -A human readable description of the entity, to be shown in Backstage. Should be kept short -and informative, suitable to give an overview of the entity's purpose at a glance. More -detailed explanations and documentation should be placed elsewhere. +A human readable description of the entity, to be shown in Backstage. Should be +kept short and informative, suitable to give an overview of the entity's purpose +at a glance. More detailed explanations and documentation should be placed +elsewhere. ### `labels` [optional] -Labels are optional key/value pairs of that are attached to the entity, and their use is -identical to [Kubernetes object labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/). +Labels are optional key/value pairs of that are attached to the entity, and +their use is identical to +[Kubernetes object labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/). -Their main purpose is for references to other entities, and for information that is -in one way or another classifying for the current entity. They are often used as values -in queries or filters. +Their main purpose is for references to other entities, and for information that +is in one way or another classifying for the current entity. They are often used +as values in queries or filters. Both the key and the value are strings, subject to the following restrictions. -Keys have an optional prefix followed by a slash, and then the name part which is required. -The prefix, if present, must be a valid lowercase domain name, at most 253 characters in total. -The name part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters -in total. +Keys have an optional prefix followed by a slash, and then the name part which +is required. The prefix, if present, must be a valid lowercase domain name, at +most 253 characters in total. The name part must be sequences of `[a-zA-Z0-9]` +separated by any of `[-_.]`, at most 63 characters in total. -The `backstage.io/` prefix is reserved for use by Backstage core components. Some keys such as -`system` also have predefined semantics. +The `backstage.io/` prefix is reserved for use by Backstage core components. +Some keys such as `system` also have predefined semantics. Values are strings that follow the same restrictions as `name` above. ### `annotations` [optional] An object with arbitrary non-identifying metadata attached to the entity, -identical in use to [Kubernetes object annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/). +identical in use to +[Kubernetes object annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/). -Their purpose is mainly, but not limited, to reference into external systems. This could -for example be a reference to the git ref the entity was ingested from, to monitoring -and logging systems, to pagerduty schedules, etc. Users may add these to descriptor -YAML files, but in addition to this automated systems may also add annotations, either -during ingestion into the catalog, or at a later time. +Their purpose is mainly, but not limited, to reference into external systems. +This could for example be a reference to the git ref the entity was ingested +from, to monitoring and logging systems, to pagerduty schedules, etc. Users may +add these to descriptor YAML files, but in addition to this automated systems +may also add annotations, either during ingestion into the catalog, or at a +later time. Both the key and the value are strings, subject to the following restrictions. -Keys have an optional prefix followed by a slash, and then the name part which is required. -The prefix must be a valid lowercase domain name if specified, at most 253 characters in total. -The name part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters -in total. +Keys have an optional prefix followed by a slash, and then the name part which +is required. The prefix must be a valid lowercase domain name if specified, at +most 253 characters in total. The name part must be sequences of `[a-zA-Z0-9]` +separated by any of `[-_.]`, at most 63 characters in total. The `backstage.io/` prefix is reserved for use by Backstage core components. @@ -218,19 +230,20 @@ Values can be of any length, but are limited to being strings. Describes the following entity kind: -| Field | Value | -| ------------ | ---------------------- | -| `apiVersion` | `backstage.io/v1beta1` | -| `kind` | `Component` | +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `Component` | -A Component describes a software component. It is typically intimately linked to the source code -that constitutes the component, and should be what a developer may regard a "unit of software", -usually with a distinct deployable or linkable artifact. +A Component describes a software component. It is typically intimately linked to +the source code that constitutes the component, and should be what a developer +may regard a "unit of software", usually with a distinct deployable or linkable +artifact. Descriptor files for this kind may look as follows. ```yaml -apiVersion: backstage.io/v1beta1 +apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: artist-web @@ -241,22 +254,22 @@ spec: owner: artist-relations@example.com ``` -In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) shape, this kind has the -following structure. +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. ### `apiVersion` and `kind` [required] -Exactly equal to `backstage.io/v1beta1` and `Component`, respectively. +Exactly equal to `backstage.io/v1alpha1` and `Component`, respectively. ### `spec.type` [required] The type of component as a string, e.g. `website`. This field is required. -The software catalog accepts any type value, but an organisation should take great -care to establish a proper taxonomy for these. Tools including Backstage itself may -read this field and behave differently depending on its value. For example, a website -type component may present tooling in the Backstage interface that is specific to just -websites. +The software catalog accepts any type value, but an organisation should take +great care to establish a proper taxonomy for these. Tools including Backstage +itself may read this field and behave differently depending on its value. For +example, a website type component may present tooling in the Backstage interface +that is specific to just websites. The current set of well-known and common values for this field is: @@ -268,31 +281,33 @@ The current set of well-known and common values for this field is: The lifecyle state of the component, e.g. `production`. This field is required. -The software catalog accepts any lifecycle value, but an organisation should take great -care to establish a proper taxonomy for these. +The software catalog accepts any lifecycle value, but an organisation should +take great care to establish a proper taxonomy for these. The current set of well-known and common values for this field is: -- `experimental` - an experiment or early, non-production component, signaling that users - may not prefer to consume it over other more established components, or that there are - low or no reliability guarantees +- `experimental` - an experiment or early, non-production component, signaling + that users may not prefer to consume it over other more established + components, or that there are low or no reliability guarantees - `production` - an established, owned, maintained component -- `deprecated` - a component that is at the end of its lifecycle, and may disappear at a - later point in time +- `deprecated` - a component that is at the end of its lifecycle, and may + disappear at a later point in time ### `spec.owner` [required] -The owner of the component, e.g. `artist-relations@example.com`. This field is required. +The owner of the component, e.g. `artist-relations@example.com`. This field is +required. -In Backstage, the owner of a component is the singular entity (commonly a team) that -bears ultimate responsibility for the component, and has the authority and capability -to develop and maintain it. They will be the point of contact if something goes wrong, -or if features are to be requested. The main purpose of this field is for display -purposes in Backstage, so that people looking at catalog items can get an understanding -of to whom this component belongs. It is not to be used by automated processes to for -example assign authorization in runtime systems. There may be others that also develop -or otherwise touch the component, but there will always be one ultimate owner. +In Backstage, the owner of a component is the singular entity (commonly a team) +that bears ultimate responsibility for the component, and has the authority and +capability to develop and maintain it. They will be the point of contact if +something goes wrong, or if features are to be requested. The main purpose of +this field is for display purposes in Backstage, so that people looking at +catalog items can get an understanding of to whom this component belongs. It is +not to be used by automated processes to for example assign authorization in +runtime systems. There may be others that also develop or otherwise touch the +component, but there will always be one ultimate owner. -Apart from being a string, the software catalog leaves the format of this field open to -implementers to choose. Most commonly, it is set to the ID or email of a group of people -in an organizational structure. +Apart from being a string, the software catalog leaves the format of this field +open to implementers to choose. Most commonly, it is set to the ID or email of a +group of people in an organizational structure. diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts index 68658e5296..68f1cec649 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts @@ -24,13 +24,13 @@ describe('DefaultNamespaceEntityPolicy', () => { beforeEach(() => { withoutNamespace = yaml.parse(` - apiVersion: backstage.io/v1beta1 + apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: my-component-yay `); withNamespace = yaml.parse(` - apiVersion: backstage.io/v1beta1 + apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: my-component-yay diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 14b44108e5..80cf70fbab 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -23,7 +23,7 @@ describe('FieldFormatEntityPolicy', () => { beforeEach(() => { data = yaml.parse(` - apiVersion: backstage.io/v1beta1 + apiVersion: backstage.io/v1alpha1 kind: Component metadata: uid: e01199ab-08cc-44c2-8e19-5c29ded82521 diff --git a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts index 50496682e8..03b0ed2eb5 100644 --- a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.test.ts @@ -23,7 +23,7 @@ describe('NoForeignRootFieldsEntityPolicy', () => { beforeEach(() => { data = yaml.parse(` - apiVersion: backstage.io/v1beta1 + apiVersion: backstage.io/v1alpha1 kind: Component metadata: uid: e01199ab-08cc-44c2-8e19-5c29ded82521 diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts index 5bc39f4e93..04acd58686 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -23,7 +23,7 @@ describe('ReservedFieldsEntityPolicy', () => { beforeEach(() => { data = yaml.parse(` - apiVersion: backstage.io/v1beta1 + apiVersion: backstage.io/v1alpha1 kind: Component metadata: uid: e01199ab-08cc-44c2-8e19-5c29ded82521 diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index 9a3c02d85a..2113772af4 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -24,7 +24,7 @@ describe('SchemaValidEntityPolicy', () => { beforeEach(() => { data = yaml.parse(` - apiVersion: backstage.io/v1beta1 + apiVersion: backstage.io/v1alpha1 kind: Component metadata: uid: e01199ab-08cc-44c2-8e19-5c29ded82521 diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts index f1a4b3c2c0..a60bb2606b 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts @@ -44,7 +44,7 @@ describe('ComponentV1alpha1Policy', () => { await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('happy path: silently accepts v1beta1 as well', async () => { + it('silently accepts v1beta1 as well', async () => { (entity as any).apiVersion = 'backstage.io/v1beta1'; await expect(policy.enforce(entity)).resolves.toBe(entity); }); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts index e3c8170af2..62837ed500 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts @@ -42,7 +42,7 @@ describe('LocationV1alpha1Policy', () => { await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('happy path: silently accepts v1beta1 as well', async () => { + it('silently accepts v1beta1 as well', async () => { (entity as any).apiVersion = 'backstage.io/v1beta1'; await expect(policy.enforce(entity)).resolves.toBe(entity); }); diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index b4dd151e72..de68a16e9c 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -173,7 +173,7 @@ describe('HigherOrderOperations', () => { target: 'thing', }; const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'c1' }, spec: { type: 'service' }, @@ -228,7 +228,7 @@ describe('HigherOrderOperations', () => { target: 'thing', }; const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'c1' }, spec: { type: 'service' }, diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index d30361d0e7..7601031699 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -38,7 +38,7 @@ describe('CatalogPage', () => { metadata: { name: 'Entity1', }, - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', }, ] as Entity[]), diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index c04e4c95a8..b50da60a50 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -22,17 +22,17 @@ import { CatalogTable } from './CatalogTable'; const entites: Entity[] = [ { - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'component1' }, }, { - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'component2' }, }, { - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'component3' }, }, diff --git a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.test.tsx b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.test.tsx index e0027431ff..4bb64f221d 100644 --- a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.test.tsx +++ b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.test.tsx @@ -22,7 +22,7 @@ import { EntityMetadataCard } from './EntityMetadataCard'; describe('EntityMetadataCard component', () => { it('should display entity name if provided', async () => { const testEntity: Entity = { - apiVersion: 'backstage.io/v1beta1', + apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { name: 'test' }, };