diff --git a/.changeset/swift-lobsters-learn.md b/.changeset/swift-lobsters-learn.md new file mode 100644 index 0000000000..afdc4e1146 --- /dev/null +++ b/.changeset/swift-lobsters-learn.md @@ -0,0 +1,8 @@ +--- +'@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..7397ce7d23 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-user) (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..6e59637c67 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -39,6 +39,7 @@ describe('GroupV1alpha1Validator', () => { }, parent: 'group-a', children: ['child-a', 'child-b'], + members: ['jdoe'], }, }; }); @@ -172,4 +173,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, + ); } /*