diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index fa4b8c23da..1ec35d10ce 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -483,6 +483,8 @@ metadata: description: Retrieve artist details spec: type: openapi + lifecycle: production + owner: artist-relations@example.com definition: | openapi: "3.0.0" info: @@ -527,6 +529,41 @@ The current set of well-known and common values for this field is: [Protocol Buffers](https://developers.google.com/protocol-buffers) to use with [gRPC](https://grpc.io/). +### `spec.lifecycle` [required] + +The lifecycle state of the API, e.g. `production`. This field is required. + +The software catalog accepts any lifecycle value, but an organization 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 API, signaling that + users may not prefer to consume it over other more established APIs, or that + there are low or no reliability guarantees +- `production` - an established, owned, maintained API +- `deprecated` - an API that is at the end of its lifecycle, and may disappear + at a later point in time + +### `spec.owner` [required] + +The owner of the API, e.g. `artist-relations@example.com`. This field is +required. + +In Backstage, the owner of an API is the singular entity (commonly a team) that +bears ultimate responsibility for the API, 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 API 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 API, 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. + ### `spec.definition` [required] The definition of the API, based on the format defined by `spec.type`. This diff --git a/packages/catalog-model/examples/hello-world-api.yaml b/packages/catalog-model/examples/hello-world-api.yaml index 298b0cb835..659c48adfb 100644 --- a/packages/catalog-model/examples/hello-world-api.yaml +++ b/packages/catalog-model/examples/hello-world-api.yaml @@ -5,6 +5,8 @@ metadata: description: Hello World example for gRPC spec: type: grpc + lifecycle: deprecated + owner: grpc@example.com definition: | // Copyright 2015 gRPC authors. // diff --git a/packages/catalog-model/examples/petstore-api.yaml b/packages/catalog-model/examples/petstore-api.yaml index 01953a4a97..c8ce576cce 100644 --- a/packages/catalog-model/examples/petstore-api.yaml +++ b/packages/catalog-model/examples/petstore-api.yaml @@ -3,8 +3,13 @@ kind: API metadata: name: petstore description: The petstore API + tags: + - store + - rest spec: type: openapi + lifecycle: experimental + owner: pets@example.com definition: | openapi: "3.0.0" info: diff --git a/packages/catalog-model/examples/streetlights-api.yaml b/packages/catalog-model/examples/streetlights-api.yaml index 4aeef993bb..d53b05fc2d 100644 --- a/packages/catalog-model/examples/streetlights-api.yaml +++ b/packages/catalog-model/examples/streetlights-api.yaml @@ -4,9 +4,11 @@ metadata: name: streetlights description: The Smartylighting Streetlights API allows you to remotely manage the city lights. tags: - - unstable + - mqtt spec: type: asyncapi + lifecycle: production + owner: streetlights@example.com definition: | asyncapi: 2.0.0 info: diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts index 48de569213..35a1c59ef8 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts @@ -33,6 +33,8 @@ describe('ApiV1alpha1Policy', () => { }, spec: { type: 'openapi', + lifecycle: 'production', + owner: 'me', definition: ` openapi: "3.0.0" info: @@ -109,6 +111,36 @@ components: 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 empty lifecycle', 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/); + }); + it('rejects missing definition', async () => { delete (entity as any).spec.definition; await expect(policy.enforce(entity)).rejects.toThrow(/definition/); diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 59e16df9a7..972f6df96d 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -26,6 +26,8 @@ export interface ApiEntityV1alpha1 extends Entity { kind: typeof KIND; spec: { type: string; + lifecycle: string; + owner: string; definition: string; }; } @@ -40,6 +42,8 @@ export class ApiEntityV1alpha1Policy implements EntityPolicy { spec: yup .object({ type: yup.string().required().min(1), + lifecycle: yup.string().required().min(1), + owner: yup.string().required().min(1), definition: yup.string().required().min(1), }) .required(),