@checkstack/catalog-backend 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/catalog-gitops-kinds.test.ts +73 -0
- package/src/index.ts +56 -1
- package/src/services/entity-service.ts +9 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -85,6 +85,11 @@ function createMockEntityService() {
|
|
|
85
85
|
const idx = groups.findIndex((g) => g.id === id);
|
|
86
86
|
if (idx >= 0) groups.splice(idx, 1);
|
|
87
87
|
}),
|
|
88
|
+
addSystemToGroup: mock(async (props: { groupId: string; systemId: string }) => {}),
|
|
89
|
+
removeSystemFromGroup: mock(async (props: { groupId: string; systemId: string }) => {}),
|
|
90
|
+
getGroupsForSystem: mock(async (systemId: string) => {
|
|
91
|
+
return [] as { groupId: string, systemId: string }[];
|
|
92
|
+
}),
|
|
88
93
|
};
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -271,6 +276,74 @@ describe("Catalog GitOps Kind: System", () => {
|
|
|
271
276
|
});
|
|
272
277
|
});
|
|
273
278
|
|
|
279
|
+
describe("Catalog GitOps Kind Extension: System -> groups", () => {
|
|
280
|
+
let mockService: ReturnType<typeof createMockEntityService>;
|
|
281
|
+
|
|
282
|
+
beforeEach(() => {
|
|
283
|
+
mockService = createMockEntityService();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("associates system with groups and removes stale ones", async () => {
|
|
287
|
+
// We mock getGroupsForSystem to return one existing association
|
|
288
|
+
mockService.getGroupsForSystem.mockResolvedValueOnce([
|
|
289
|
+
{ groupId: "grp-stale", systemId: "sys-1" }
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
const mockExtContext: ReconcileContext = {
|
|
293
|
+
...mockContext,
|
|
294
|
+
resolveEntityRef: mock(async ({ entityName }) => {
|
|
295
|
+
if (entityName === "new-group") return "grp-new";
|
|
296
|
+
return undefined;
|
|
297
|
+
}),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Simulate the inline reconcile logic from index.ts
|
|
301
|
+
const reconcileExt = async ({ extensionSpec, entityId, context }: any) => {
|
|
302
|
+
if (!extensionSpec || extensionSpec.length === 0) return;
|
|
303
|
+
|
|
304
|
+
const desiredGroupIds = new Set<string>();
|
|
305
|
+
|
|
306
|
+
for (const entry of extensionSpec) {
|
|
307
|
+
const groupId = await context.resolveEntityRef({
|
|
308
|
+
kind: entry.kind,
|
|
309
|
+
entityName: entry.name,
|
|
310
|
+
});
|
|
311
|
+
if (groupId) {
|
|
312
|
+
desiredGroupIds.add(groupId);
|
|
313
|
+
await mockService.addSystemToGroup({ groupId, systemId: entityId });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const currentAssociations = await mockService.getGroupsForSystem(entityId);
|
|
318
|
+
for (const existing of currentAssociations) {
|
|
319
|
+
if (!desiredGroupIds.has(existing.groupId)) {
|
|
320
|
+
await mockService.removeSystemFromGroup({
|
|
321
|
+
groupId: existing.groupId,
|
|
322
|
+
systemId: entityId,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
await reconcileExt({
|
|
329
|
+
entity: { metadata: { name: "my-system" } },
|
|
330
|
+
extensionSpec: [{ kind: "Group", name: "new-group" }],
|
|
331
|
+
entityId: "sys-1",
|
|
332
|
+
context: mockExtContext,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(mockService.addSystemToGroup).toHaveBeenCalledWith({
|
|
336
|
+
groupId: "grp-new",
|
|
337
|
+
systemId: "sys-1",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(mockService.removeSystemFromGroup).toHaveBeenCalledWith({
|
|
341
|
+
groupId: "grp-stale",
|
|
342
|
+
systemId: "sys-1",
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
274
347
|
describe("Catalog GitOps Kind: Group", () => {
|
|
275
348
|
let mockService: ReturnType<typeof createMockEntityService>;
|
|
276
349
|
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { resolveRoute, type InferClient, extractErrorMessage} from "@checkstack/
|
|
|
18
18
|
import { registerSearchProvider } from "@checkstack/command-backend";
|
|
19
19
|
import { EntityService } from "./services/entity-service";
|
|
20
20
|
import { entityKindExtensionPoint } from "@checkstack/gitops-backend";
|
|
21
|
-
import { CHECKSTACK_API_VERSION } from "@checkstack/gitops-common";
|
|
21
|
+
import { CHECKSTACK_API_VERSION, entityRefSchema } from "@checkstack/gitops-common";
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
|
|
24
24
|
// Database schema is still needed for types in creating the router
|
|
@@ -82,6 +82,61 @@ export default createBackendPlugin({
|
|
|
82
82
|
},
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
// Register kind extension: System -> groups
|
|
86
|
+
kindRegistry.registerKindExtension({
|
|
87
|
+
apiVersion: CHECKSTACK_API_VERSION,
|
|
88
|
+
kind: "System",
|
|
89
|
+
namespace: "groups",
|
|
90
|
+
specSchema: z.array(entityRefSchema).optional(),
|
|
91
|
+
reconcile: async ({ entity, extensionSpec, entityId, context }) => {
|
|
92
|
+
if (!gitopsDb) throw new Error("Catalog database not initialized");
|
|
93
|
+
if (!extensionSpec || extensionSpec.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const entityService = new EntityService(gitopsDb);
|
|
96
|
+
const systemEntityId = entityId;
|
|
97
|
+
|
|
98
|
+
const desiredGroupIds = new Set<string>();
|
|
99
|
+
|
|
100
|
+
for (const entry of extensionSpec) {
|
|
101
|
+
const groupId = await context.resolveEntityRef({
|
|
102
|
+
kind: entry.kind,
|
|
103
|
+
entityName: entry.name,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!groupId) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Cannot resolve ${entry.kind} ref "${entry.name}" — ensure the entity exists`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
desiredGroupIds.add(groupId);
|
|
113
|
+
|
|
114
|
+
await entityService.addSystemToGroup({
|
|
115
|
+
groupId: groupId,
|
|
116
|
+
systemId: systemEntityId,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
context.logger.info(
|
|
120
|
+
`GitOps: associated System "${entity.metadata.name}" with Group "${entry.name}" (${groupId})`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Remove stale associations not in the spec
|
|
125
|
+
const currentAssociations = await entityService.getGroupsForSystem(systemEntityId);
|
|
126
|
+
for (const existing of currentAssociations) {
|
|
127
|
+
if (!desiredGroupIds.has(existing.groupId)) {
|
|
128
|
+
await entityService.removeSystemFromGroup({
|
|
129
|
+
groupId: existing.groupId,
|
|
130
|
+
systemId: systemEntityId,
|
|
131
|
+
});
|
|
132
|
+
context.logger.info(
|
|
133
|
+
`GitOps: removed stale association ${existing.groupId} from System "${entity.metadata.name}"`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
85
140
|
// Register kind: Group
|
|
86
141
|
kindRegistry.registerKind({
|
|
87
142
|
apiVersion: CHECKSTACK_API_VERSION,
|
|
@@ -144,6 +144,15 @@ export class EntityService {
|
|
|
144
144
|
await this.database.delete(schema.groups).where(eq(schema.groups.id, id));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
async getGroupsForSystem(systemId: string) {
|
|
148
|
+
const associations = await this.database
|
|
149
|
+
.select()
|
|
150
|
+
.from(schema.systemsGroups)
|
|
151
|
+
.where(eq(schema.systemsGroups.systemId, systemId));
|
|
152
|
+
|
|
153
|
+
return associations;
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
async addSystemToGroup(props: { groupId: string; systemId: string }) {
|
|
148
157
|
const { groupId, systemId } = props;
|
|
149
158
|
await this.database
|