diff --git a/.changeset/swift-lobsters-learn.md b/.changeset/swift-lobsters-learn.md new file mode 100644 index 0000000000..e5adfde478 --- /dev/null +++ b/.changeset/swift-lobsters-learn.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +--- + +Added support for the "members" field of the Group entity, allowing specification of direct members from the Group side of the relationship. Added support to the BuiltinKindsEntityProcessor to generate the appropriate relationships. diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index e4b046d76f..5c06401642 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -808,6 +808,7 @@ spec: picture: https://example.com/groups/bu-infrastructure.jpeg parent: ops children: [backstage, other] + members: [jdoe] ``` In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) @@ -865,6 +866,18 @@ The entries of this array are | --------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | | [`Group`](#kind-group) (default) | Same as this entity, typically `default` | [`hasMember`, and reverse `memberOf`](well-known-relations.md#memberof-and-hasmember) | +### `spec.members` [optional] + +The users that are direct members of this group. The items are not guaranteed to +be ordered in any particular way. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references). + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | +| [`User`](#kind-group) (default) | Same as this entity, typically `default` | [`hasMember`, and reverse `memberOf`](well-known-relations.md#memberof-and-hasmember) | + ## Kind: User Describes the following entity kind: diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index b25795cd31..aa8011e977 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -172,4 +172,26 @@ describe('GroupV1alpha1Validator', () => { (entity as any).spec.children = []; await expect(validator.check(entity)).resolves.toBe(true); }); + + // members + + it('accepts missing members', async () => { + delete (entity as any).spec.members; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects empty members', async () => { + (entity as any).spec.members = ['']; + await expect(validator.check(entity)).rejects.toThrow(/members/); + }); + + it('rejects undefined members', async () => { + (entity as any).spec.members = [undefined]; + await expect(validator.check(entity)).rejects.toThrow(/members/); + }); + + it('accepts no members', async () => { + (entity as any).spec.members = []; + await expect(validator.check(entity)).resolves.toBe(true); + }); }); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index d039fdaba8..74ca4f221a 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -36,6 +36,7 @@ export interface GroupEntityV1alpha1 extends Entity { }; parent?: string; children: string[]; + members?: string[]; }; } diff --git a/packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json index fc19d98301..14c9d27eef 100644 --- a/packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json @@ -86,6 +86,15 @@ "examples": ["backstage", "other"], "minLength": 1 } + }, + "members": { + "type": "array", + "description": "The users that are members of this group. The entries of this array are entity references.", + "items": { + "type": "string", + "examples": ["jdoe"], + "minLength": 1 + } } } } diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts index feb4791477..2162272390 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts @@ -353,12 +353,13 @@ describe('BuiltinKindsEntityProcessor', () => { type: 't', parent: 'p', children: ['c'], + members: ['m'], }, }; await processor.postProcessEntity(entity, location, emit); - expect(emit).toBeCalledTimes(4); + expect(emit).toBeCalledTimes(6); expect(emit).toBeCalledWith({ type: 'relation', relation: { @@ -391,6 +392,22 @@ describe('BuiltinKindsEntityProcessor', () => { target: { kind: 'Group', namespace: 'default', name: 'c' }, }, }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'User', namespace: 'default', name: 'm' }, + type: 'memberOf', + target: { kind: 'Group', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'n' }, + type: 'hasMember', + target: { kind: 'User', namespace: 'default', name: 'm' }, + }, + }); }); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index 26a8538707..64a47127f9 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -226,6 +226,12 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { RELATION_PARENT_OF, RELATION_CHILD_OF, ); + doEmit( + group.spec.members, + { defaultKind: 'User', defaultNamespace: selfRef.namespace }, + RELATION_HAS_MEMBER, + RELATION_MEMBER_OF, + ); } /*