Implement Harbor gateway device ecosystem support
Build and Deploy Gateway / build-and-push (push) Successful in 26s
Build and Deploy Gateway / deploy (push) Successful in 14s

This commit is contained in:
2026-08-31 02:06:22 +03:00
parent 7e15cc199f
commit 116686a138
21 changed files with 1924 additions and 51 deletions
+138
View File
@@ -1443,3 +1443,141 @@ test('direct IPv4 counters stay metrics-only and keep the last good transport sn
assert.deepEqual(service.metricsSnapshot().directTraffic.series, []);
assert.equal(service.metricsSnapshot().directTraffic.source.error, null);
});
test('device tag catalog validates, orders and mutates assignments under one revision', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-tags-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
const observedAt = '2026-08-31T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.10', mac, interface: 'br0', observedAt, active: true }],
}),
now: () => new Date(observedAt),
});
let snapshot = await service.refresh();
snapshot = service.createTag(' Home ', snapshot.revision);
const home = snapshot.tags[0];
assert.match(home.id, /^tag_[a-f0-9]{16}$/);
assert.equal(home.name, 'Home');
assert.throws(
() => service.createTag('home', snapshot.revision),
(error) => error.code === 'DEVICE_TAG_NAME_CONFLICT',
);
assert.throws(
() => service.createTag('x'.repeat(25), snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
snapshot = service.createTag('Work', snapshot.revision);
const work = snapshot.tags[1];
snapshot = service.update(snapshot.devices[0].id, { tagIds: [work.id, home.id] }, snapshot.revision);
assert.deepEqual(snapshot.devices[0].tagIds, [home.id, work.id]);
assert.deepEqual(store.read().tags.byMac[mac], [home.id, work.id]);
assert.throws(
() => service.update(snapshot.devices[0].id, { tagIds: [home.id, work.id] }, snapshot.revision - 1),
(error) => error.code === 'STATE_CONFLICT',
);
assert.equal(
service.update(snapshot.devices[0].id, { tagIds: [home.id, work.id] }, snapshot.revision).revision,
snapshot.revision,
);
snapshot = service.renameTag(home.id, 'HOME', snapshot.revision);
const renamedRevision = snapshot.revision;
assert.equal(snapshot.tags[0].name, 'HOME');
assert.throws(
() => service.renameTag(home.id, 'HOME', renamedRevision - 1),
(error) => error.code === 'STATE_CONFLICT',
);
assert.equal(service.renameTag(home.id, 'HOME', renamedRevision).revision, renamedRevision);
assert.throws(
() => service.update(snapshot.devices[0].id, { tagIds: ['tag_ffffffffffffffff'] }, renamedRevision),
(error) => error.code === 'DEVICE_TAG_NOT_FOUND',
);
while (snapshot.tags.length < 9) snapshot = service.createTag(`Tag ${snapshot.tags.length}`, snapshot.revision);
snapshot = service.update(snapshot.devices[0].id, { tagIds: snapshot.tags.slice(0, 8).map(({ id }) => id) }, snapshot.revision);
assert.throws(
() => service.update(snapshot.devices[0].id, { tagIds: snapshot.tags.slice(0, 9).map(({ id }) => id) }, snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
snapshot = service.deleteTag(home.id, snapshot.revision);
assert.equal(snapshot.tags.some(({ id }) => id === home.id), false);
assert.equal(snapshot.devices[0].tagIds.includes(home.id), false);
while (snapshot.tags.length < 32) snapshot = service.createTag(`More ${snapshot.tags.length}`, snapshot.revision);
assert.throws(
() => service.createTag('Overflow', snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
});
test('tag migration recovers safe values and retention removes assignment but keeps catalog', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-tag-retention-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
let current = new Date('2026-07-01T12:00:00.000Z');
const mac = '00:11:22:33:44:66';
let present = true;
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt: current.toISOString(),
error: null,
observations: present
? [{ ip: '192.168.50.12', mac, interface: 'br0', observedAt: current.toISOString(), active: true }]
: [],
}),
now: () => current,
});
let snapshot = await service.refresh();
snapshot = service.createTag('Дом', snapshot.revision);
const tag = snapshot.tags[0];
snapshot = service.update(snapshot.devices[0].id, { tagIds: [tag.id] }, snapshot.revision);
const recovered = migrateDeviceInventoryState({
...store.read(),
tags: {
schemaVersion: 1,
items: [tag, { ...tag, id: 'tag_ffffffffffffffff' }, { id: 'bad', name: 'Bad' }],
byMac: { [mac.toUpperCase()]: [tag.id, tag.id, 'tag_ffffffffffffffff'] },
},
});
assert.deepEqual(recovered.tags, {
schemaVersion: 1,
items: [tag],
byMac: { [mac]: [tag.id] },
});
assert.throws(
() => migrateDeviceInventoryState({ ...store.read(), tags: { schemaVersion: 2 } }),
/Unsupported device tags schemaVersion/,
);
present = false;
current = new Date('2026-08-02T12:00:00.000Z');
snapshot = await service.refresh();
assert.equal(snapshot.devices.length, 0);
assert.deepEqual(snapshot.tags, [tag]);
assert.deepEqual(store.read().tags.byMac, {});
present = true;
current = new Date('2026-08-03T12:00:00.000Z');
snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].tagIds, []);
assert.deepEqual(snapshot.tags, [tag]);
});