diff --git a/js/ui.js b/js/ui.js index 7156fc5..a7ddd7f 100644 --- a/js/ui.js +++ b/js/ui.js @@ -463,6 +463,13 @@ async function loadJobDetail(jobId) { +
+ +
${_renderJobConfigFields(job.key, cfg)}
@@ -521,10 +528,12 @@ async function saveJobDetail(jobId) { const apiKey = document.getElementById('job-cfg-api-key'); const apiUrl = document.getElementById('job-cfg-api-url'); const apiToken = document.getElementById('job-cfg-api-token'); - if (tailnet) cfg.tailnet = tailnet.value.trim(); - if (apiKey) cfg.api_key = apiKey.value; - if (apiUrl) cfg.api_url = apiUrl.value.trim(); - if (apiToken) cfg.api_token = apiToken.value; + if (tailnet) cfg.tailnet = tailnet.value.trim(); + if (apiKey) cfg.api_key = apiKey.value; + if (apiUrl) cfg.api_url = apiUrl.value.trim(); + if (apiToken) cfg.api_token = apiToken.value; + const runOnCreate = document.getElementById('job-run-on-create'); + if (runOnCreate) cfg.run_on_create = runOnCreate.checked; const res = await fetch(`/api/jobs/${jobId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, diff --git a/server/jobs.js b/server/jobs.js index cfcb20e..f6b1a59 100644 --- a/server/jobs.js +++ b/server/jobs.js @@ -129,6 +129,13 @@ export async function runJob(jobId) { const _intervals = new Map(); +export async function runJobsOnCreate() { + for (const job of getJobs()) { + const cfg = JSON.parse(job.config || '{}'); + if (cfg.run_on_create) runJob(job.id).catch(e => console.error(`runJobsOnCreate job ${job.id}:`, e)); + } +} + export function restartJobs() { for (const iv of _intervals.values()) clearInterval(iv); _intervals.clear(); diff --git a/server/routes.js b/server/routes.js index eed881c..fba5552 100644 --- a/server/routes.js +++ b/server/routes.js @@ -5,7 +5,7 @@ import { getConfig, setConfig, getJobs, getJob, updateJob, getJobRuns, getAllJobs, getAllJobRuns, importJobs, } from './db.js'; -import { runJob, restartJobs } from './jobs.js'; +import { runJob, restartJobs, runJobsOnCreate } from './jobs.js'; export const router = Router(); @@ -102,6 +102,7 @@ router.post('/instances', (req, res) => { createInstance(data); const created = getInstance(data.vmid); res.status(201).json(created); + runJobsOnCreate().catch(() => {}); } catch (e) { handleDbError('POST /api/instances', e, res); } diff --git a/tests/api.test.js b/tests/api.test.js index 6b7e652..5373d62 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -623,6 +623,25 @@ describe('POST /api/jobs/:id/run', () => { expect(res.status).toBe(500) }) + it('run_on_create: triggers matching jobs when an instance is created', async () => { + createJob({ ...testJob, config: JSON.stringify({ api_key: 'k', tailnet: 't', run_on_create: true }) }) + const id = (await request(app).get('/api/jobs')).body[0].id + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ devices: [] }) })) + await request(app).post('/api/instances').send(base) + await new Promise(r => setImmediate(r)) + const detail = await request(app).get(`/api/jobs/${id}`) + expect(detail.body.runs).toHaveLength(1) + expect(detail.body.runs[0].status).toBe('success') + }) + + it('run_on_create: does not trigger jobs without the flag', async () => { + createJob(testJob) + const id = (await request(app).get('/api/jobs')).body[0].id + await request(app).post('/api/instances').send(base) + await new Promise(r => setImmediate(r)) + expect((await request(app).get(`/api/jobs/${id}`)).body.runs).toHaveLength(0) + }) + it('semaphore_sync: parses ansible inventory and updates instances', async () => { const semaphoreJob = { key: 'semaphore_sync', name: 'Semaphore Sync', description: 'test',