Skip to content

Commit 3ac0be1

Browse files
authored
fix: ensure docker compose trigger labels are handled correctly (#1)
1 parent 0e31edf commit 3ac0be1

File tree

6 files changed

+401
-5
lines changed

6 files changed

+401
-5
lines changed

app/package-lock.json

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/registry/index.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,91 @@ test('registerTriggers should warn when registration errors occur', async () =>
295295
);
296296
});
297297

298+
test('ensureDockercomposeTriggerForContainer should create a trigger with container name only when no compose path', async () => {
299+
const triggerId = await registry.ensureDockercomposeTriggerForContainer('my-service');
300+
expect(triggerId).toBe('dockercompose.my-service');
301+
expect(Object.keys(registry.getState().trigger)).toContain(triggerId);
302+
});
303+
304+
test('ensureDockercomposeTriggerForContainer should create trigger with parent folder and container name', async () => {
305+
const triggerId = await registry.ensureDockercomposeTriggerForContainer('my-service', '/home/user/myapp/docker-compose.yml');
306+
expect(triggerId).toBe('dockercompose.myapp-my-service');
307+
expect(Object.keys(registry.getState().trigger)).toContain(triggerId);
308+
});
309+
310+
test('ensureDockercomposeTriggerForContainer should append a number when name conflicts', async () => {
311+
const triggerId1 = await registry.ensureDockercomposeTriggerForContainer('my-service');
312+
const triggerId2 = await registry.ensureDockercomposeTriggerForContainer('my-service');
313+
314+
expect(triggerId1).toBe('dockercompose.my-service');
315+
expect(triggerId2).toBe('dockercompose.my-service-2');
316+
});
317+
318+
test('ensureDockercomposeTriggerForContainer should append a number when name conflicts with compose path', async () => {
319+
const triggerId1 = await registry.ensureDockercomposeTriggerForContainer('my-service', '/home/user/myapp/docker-compose.yml');
320+
const triggerId2 = await registry.ensureDockercomposeTriggerForContainer('my-service', '/home/user/myapp/docker-compose.yml');
321+
322+
expect(triggerId1).toBe('dockercompose.myapp-my-service');
323+
expect(triggerId2).toBe('dockercompose.myapp-my-service-2');
324+
});
325+
326+
test('ensureDockercomposeTriggerForContainer should handle Windows paths', async () => {
327+
const triggerId = await registry.ensureDockercomposeTriggerForContainer('my-service', 'C:\\Users\\user\\myapp\\docker-compose.yml');
328+
expect(triggerId).toBe('dockercompose.myapp-my-service');
329+
expect(Object.keys(registry.getState().trigger)).toContain(triggerId);
330+
});
331+
332+
test('ensureDockercomposeTriggerForContainer should handle paths without parent folder', async () => {
333+
const triggerId = await registry.ensureDockercomposeTriggerForContainer('my-service', '/docker-compose.yml');
334+
// When path has no parent folder (slice(-2, -1)[0] returns undefined for single-segment paths),
335+
// falls back to container name only
336+
expect(triggerId).toBe('dockercompose.my-service');
337+
expect(Object.keys(registry.getState().trigger)).toContain(triggerId);
338+
});
339+
340+
test('sanitizeComponentName should handle empty string', () => {
341+
const result = registry.testable_sanitizeComponentName('');
342+
expect(result).toBe('container');
343+
});
344+
345+
test('sanitizeComponentName should handle strings with only special characters', () => {
346+
const result = registry.testable_sanitizeComponentName('@@@###$$$');
347+
// Result should be composed only of safe characters for component names
348+
expect(result).toMatch(/^[a-z0-9._-]*$/);
349+
});
350+
351+
test('sanitizeComponentName should lowercase and trim mixed-case names with whitespace', () => {
352+
const input = ' My-Component_Name ';
353+
const result = registry.testable_sanitizeComponentName(input);
354+
355+
// Should be all lowercase
356+
expect(result).toBe(result.toLowerCase());
357+
// Should not have leading or trailing whitespace
358+
expect(result.startsWith(' ')).toBe(false);
359+
expect(result.endsWith(' ')).toBe(false);
360+
expect(result).toBe('my-component_name');
361+
});
362+
363+
test('sanitizeComponentName should handle various special characters', () => {
364+
const input = 'Comp@#Name!$ With%Chars';
365+
const result = registry.testable_sanitizeComponentName(input);
366+
367+
// Should be lowercase and contain only safe characters
368+
expect(result).toBe(result.toLowerCase());
369+
expect(result).toMatch(/^[a-z0-9._-]*$/);
370+
expect(result).toBe('comp--name---with-chars');
371+
});
372+
373+
test('sanitizeComponentName should handle unicode and symbols robustly', () => {
374+
const input = 'Üñïçødë-µ_Service!';
375+
const result = registry.testable_sanitizeComponentName(input);
376+
377+
// Should be lowercase and contain only safe characters
378+
expect(result).toBe(result.toLowerCase());
379+
expect(result).toMatch(/^[a-z0-9._-]*$/);
380+
});
381+
382+
298383
test('registerWatchers should register all watchers', async () => {
299384
watchers = {
300385
watcher1: {

app/registry/index.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ const state: RegistryState = {
6868
agent: {},
6969
};
7070

71+
const CONTAINER_TRIGGER_DEFAULT_NAME = 'container';
72+
7173
export function getState() {
7274
return state;
7375
}
@@ -269,6 +271,67 @@ async function registerTriggers(options: RegistrationOptions = {}) {
269271
}
270272
}
271273

274+
function sanitizeComponentName(name: string): string {
275+
const nameSanitized = `${name || ''}`
276+
.trim()
277+
.toLowerCase()
278+
.replace(/[^a-z0-9._-]/g, '-');
279+
return nameSanitized || CONTAINER_TRIGGER_DEFAULT_NAME;
280+
}
281+
282+
/**
283+
* Ensure a dockercompose trigger exists for a container name.
284+
* Name collision strategy: append a number to the trigger name.
285+
*/
286+
export async function ensureDockercomposeTriggerForContainer(
287+
containerName: string,
288+
composeFilePath?: string,
289+
): Promise<string> {
290+
let triggerBaseName: string;
291+
292+
if (composeFilePath) {
293+
// Extract parent folder name from compose file path
294+
// slice(-2, -1)[0] gets the second-to-last path segment (the parent folder)
295+
// Returns undefined for root-level files, which becomes empty string via || ''
296+
const parentFolder = composeFilePath
297+
.replace(/\\/g, '/') // normalize Windows paths
298+
.split('/')
299+
.filter((part) => part.length > 0)
300+
.slice(-2, -1)[0] || ''; // Get the parent folder name
301+
302+
const sanitizedFolder = sanitizeComponentName(parentFolder);
303+
const sanitizedContainer = sanitizeComponentName(containerName);
304+
305+
// Only use folder prefix if parent folder exists and is not a default fallback value
306+
// (sanitizeComponentName returns 'container' as default when input is empty/invalid)
307+
if (sanitizedFolder && sanitizedFolder !== CONTAINER_TRIGGER_DEFAULT_NAME) {
308+
triggerBaseName = `${sanitizedFolder}-${sanitizedContainer}`;
309+
} else {
310+
triggerBaseName = sanitizedContainer;
311+
}
312+
} else {
313+
triggerBaseName = sanitizeComponentName(containerName);
314+
}
315+
316+
let triggerName = triggerBaseName;
317+
let conflictIndex = 2;
318+
319+
while (state.trigger[`dockercompose.${triggerName}`]) {
320+
triggerName = `${triggerBaseName}-${conflictIndex}`;
321+
conflictIndex += 1;
322+
}
323+
324+
const triggerRegistered = await registerComponent({
325+
kind: 'trigger',
326+
provider: 'dockercompose',
327+
name: triggerName,
328+
configuration: {},
329+
componentPath: 'triggers/providers',
330+
});
331+
332+
return triggerRegistered.getId();
333+
}
334+
272335
/**
273336
* Register registries.
274337
* @returns {Promise}
@@ -491,6 +554,7 @@ export {
491554
deregisterWatchers as testable_deregisterWatchers,
492555
deregisterAuthentications as testable_deregisterAuthentications,
493556
deregisterAll as testable_deregisterAll,
557+
sanitizeComponentName as testable_sanitizeComponentName,
494558
shutdown as testable_shutdown,
495559
applyTriggerGroupDefaults as testable_applyTriggerGroupDefaults,
496560
getKnownProviderSet as testable_getKnownProviderSet,

0 commit comments

Comments
 (0)