Skip to content

Commit b5aad54

Browse files
committed
feat(ui): Validate task payload before creation
Adds functionality to validate task payload in UI. Docker compose provides correct defaults in task view.
1 parent c55407f commit b5aad54

File tree

9 files changed

+295
-42
lines changed

9 files changed

+295
-42
lines changed

changelog/issue-5577.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
audience: general
2+
level: patch
3+
reference: issue 5577
4+
---
5+
6+
Adds linting functionality in the Create Task page.
7+
8+
Validates create task and its payload based on the selected worker type.

docker-compose.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ services:
9797
GRAPHQL_SUBSCRIPTION_ENDPOINT: http://taskcluster/graphql
9898
GRAPHQL_ENDPOINT: http://taskcluster/graphql
9999
UI_LOGIN_STRATEGY_NAMES: local
100+
SITE_SPECIFIC: '{"tutorial_worker_pool_id":"docker-compose/generic-worker","tutorial_worker_schema":"generic-simple-posix"}'
100101
command: ui/web
101102
ports:
102103
- '3022:80'
@@ -183,7 +184,7 @@ services:
183184
TRUST_PROXY: 'true'
184185
READ_DB_URL: postgresql://postgres@postgres:5432/taskcluster
185186
WRITE_DB_URL: postgresql://postgres@postgres:5432/taskcluster
186-
DB_CRYPTO_KEYS: '[{"id": "dev-init", "algo": "aes-256", "key": "AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
187+
DB_CRYPTO_KEYS: '[{"id":"dev-init","algo":"aes-256","key":"AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
187188
PULSE_USERNAME: admin
188189
PULSE_PASSWORD: admin
189190
PULSE_HOSTNAME: rabbitmq
@@ -272,7 +273,7 @@ services:
272273
environment:
273274
READ_DB_URL: postgresql://postgres@postgres:5432/taskcluster
274275
WRITE_DB_URL: postgresql://postgres@postgres:5432/taskcluster
275-
DB_CRYPTO_KEYS: '[{"id": "dev-init", "algo": "aes-256", "key": "AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
276+
DB_CRYPTO_KEYS: '[{"id":"dev-init","algo":"aes-256","key":"AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
276277
TASKCLUSTER_ROOT_URL: http://taskcluster
277278
TASKCLUSTER_CLIENT_ID: static/taskcluster/hooks
278279
TASKCLUSTER_ACCESS_TOKEN: j2Z6zW2QSLehailBXlosdw9e2Ti8R_Qh2M4buAEQfsMA
@@ -387,7 +388,7 @@ services:
387388
TASKCLUSTER_ACCESS_TOKEN: j2Z6zW2QSLehailBXlosdw9e2Ti8R_Qh2M4buAEQfsMA
388389
READ_DB_URL: postgresql://postgres@postgres:5432/taskcluster
389390
WRITE_DB_URL: postgresql://postgres@postgres:5432/taskcluster
390-
DB_CRYPTO_KEYS: '[{"id": "dev-init", "algo": "aes-256", "key": "AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
391+
DB_CRYPTO_KEYS: '[{"id":"dev-init","algo":"aes-256","key":"AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
391392
LEVEL: warning
392393
PORT: 80
393394
NODE_ENV: development
@@ -496,7 +497,7 @@ services:
496497
TASKCLUSTER_ACCESS_TOKEN: j2Z6zW2QSLehailBXlosdw9e2Ti8R_Qh2M4buAEQfsMA
497498
READ_DB_URL: postgresql://postgres@postgres:5432/taskcluster
498499
WRITE_DB_URL: postgresql://postgres@postgres:5432/taskcluster
499-
DB_CRYPTO_KEYS: '[{"id": "dev-init", "algo": "aes-256", "key": "AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
500+
DB_CRYPTO_KEYS: '[{"id":"dev-init","algo":"aes-256","key":"AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
500501
LEVEL: warning
501502
PORT: 80
502503
NODE_ENV: development
@@ -532,7 +533,7 @@ services:
532533
TASKCLUSTER_ACCESS_TOKEN: j2Z6zW2QSLehailBXlosdw9e2Ti8R_Qh2M4buAEQfsMA
533534
READ_DB_URL: postgresql://postgres@postgres:5432/taskcluster
534535
WRITE_DB_URL: postgresql://postgres@postgres:5432/taskcluster
535-
DB_CRYPTO_KEYS: '[{"id": "dev-init", "algo": "aes-256", "key": "AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
536+
DB_CRYPTO_KEYS: '[{"id":"dev-init","algo":"aes-256","key":"AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
536537
PULSE_USERNAME: admin
537538
PULSE_PASSWORD: admin
538539
PULSE_HOSTNAME: rabbitmq
@@ -568,7 +569,7 @@ services:
568569
TASKCLUSTER_ACCESS_TOKEN: j2Z6zW2QSLehailBXlosdw9e2Ti8R_Qh2M4buAEQfsMA
569570
READ_DB_URL: postgresql://postgres@postgres:5432/taskcluster
570571
WRITE_DB_URL: postgresql://postgres@postgres:5432/taskcluster
571-
DB_CRYPTO_KEYS: '[{"id": "dev-init", "algo": "aes-256", "key": "AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
572+
DB_CRYPTO_KEYS: '[{"id":"dev-init","algo":"aes-256","key":"AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]'
572573
PORT: 80
573574
NODE_ENV: development
574575
FORCE_SSL: 'false'

infrastructure/tooling/src/generate/generators/docker-compose.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,14 @@ const defaultValues = {
7979
GRAPHQL_ENDPOINT: `http://taskcluster/graphql`,
8080
GRAPHQL_SUBSCRIPTION_ENDPOINT: `http://taskcluster/graphql`,
8181
UI_LOGIN_STRATEGY_NAMES: 'local',
82+
SITE_SPECIFIC: JSON.stringify({
83+
tutorial_worker_pool_id: 'docker-compose/generic-worker',
84+
tutorial_worker_schema: 'generic-simple-posix',
85+
}),
8286

8387
// Auth
8488
STATIC_CLIENTS: JSON.stringify(staticClients),
85-
DB_CRYPTO_KEYS: '[{"id": "dev-init", "algo": "aes-256", "key": "AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4="}]',
89+
DB_CRYPTO_KEYS: JSON.stringify([{ id: 'dev-init', algo: 'aes-256', key: 'AUZzegzU1Xp3dW2tPRU615HXI04oJTt9NDIokH3HXN4=' }]),
8690

8791
// Worker Manager
8892
PROVIDERS: JSON.stringify(workerManagerProviders),

ui/.neutrinorc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ module.exports = {
6969
target: proxyTarget,
7070
changeOrigin: true,
7171
},
72+
'/schemas': {
73+
target: proxyTarget,
74+
changeOrigin: true,
75+
},
7276
'/subscription': {
7377
ws: true,
7478
changeOrigin: true,

ui/docs/manual/deploying/ui.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ ui:
2424
github_app_url: ..
2525
# A workerPoolId that most people have access to, for use in the documentation tutorial
2626
tutorial_worker_pool_id: ..
27+
# know in advance which payload schema to use, see constants.js
28+
tutorial_worker_schema: ..
2729
# user-visible usernames for notify
2830
notify_email_sender: ..
2931
notify_matrix_bot_name: ..

ui/src/utils/constants.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,58 @@ export const NULL_WORKER_POOL = {
284284
config: {},
285285
};
286286
export const UI_SCHEDULER_ID = 'taskcluster-ui';
287+
288+
const payloadCommand = [
289+
'/bin/bash',
290+
'-c',
291+
'for ((i=1;i<=600;i++)); do echo $i; sleep 1; done',
292+
];
293+
294+
export const TASK_PAYLOAD_SCHEMAS = {
295+
'docker-worker': {
296+
label: 'Docker worker',
297+
type: 'docker-worker',
298+
schema: 'v1/payload.json',
299+
samplePayload: {
300+
image: 'ubuntu:latest',
301+
command: payloadCommand,
302+
maxRunTime: 600 + 30,
303+
},
304+
},
305+
'generic-simple-posix': {
306+
label: 'Generic worker simple posix',
307+
type: 'generic-worker',
308+
schema: 'simple_posix.json',
309+
samplePayload: {
310+
command: [payloadCommand],
311+
maxRunTime: 600 + 30,
312+
},
313+
},
314+
'generic-multi-win': {
315+
label: 'Generic worker multiuser windows',
316+
type: 'generic-worker',
317+
schema: 'multiuser_windows.json',
318+
samplePayload: {
319+
command: ['dir'],
320+
maxRunTime: 600 + 30,
321+
},
322+
},
323+
'generic-multi-posix': {
324+
label: 'Generic worker multiuser posix',
325+
type: 'generic-worker',
326+
schema: 'multiuser_posix.json',
327+
samplePayload: {
328+
command: [payloadCommand],
329+
maxRunTime: 600 + 30,
330+
},
331+
},
332+
'generic-docker-posix': {
333+
label: 'Generic worker docker posix',
334+
type: 'generic-worker',
335+
schema: 'docker_posix.json',
336+
samplePayload: {
337+
command: [payloadCommand],
338+
maxRunTime: 600 + 30,
339+
},
340+
},
341+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import urls from 'taskcluster-lib-urls';
2+
import ajv from './ajv';
3+
4+
const schemas = {};
5+
const fetchSchema = async (service, schema) => {
6+
const res = await fetch(urls.schema('', service, schema));
7+
8+
return res.json();
9+
};
10+
11+
const prefetch = async () => {
12+
ajv.addSchema(await fetchSchema('common', 'metaschema.json'));
13+
ajv.addSchema(await fetchSchema('queue', 'v1/task.json'));
14+
ajv.addSchema(await fetchSchema('queue', 'v1/task-metadata.json'));
15+
};
16+
17+
const prefetchPromise = prefetch();
18+
19+
export const formatErrorDetails = error => {
20+
const msg = [error.message];
21+
22+
if (error.keyword === 'type') {
23+
msg.push(`'${error.instancePath}'`);
24+
} else if (error.keyword === 'additionalProperties') {
25+
msg.push(`'${error.params.additionalProperty}'`);
26+
}
27+
28+
return msg.join(' ');
29+
};
30+
31+
export default async (value, service, schema) => {
32+
await prefetchPromise;
33+
34+
if (!schemas['create-task']) {
35+
schemas['create-task'] = await fetchSchema(
36+
'queue',
37+
'v1/create-task-request.json'
38+
);
39+
}
40+
41+
const errors = [];
42+
const taskValidation = ajv.validate(schemas['create-task'], value);
43+
44+
if (!taskValidation) {
45+
(ajv.errors || []).forEach(error => {
46+
errors.push(`Task ${formatErrorDetails(error)}`);
47+
});
48+
}
49+
50+
// allow to validate create task payload only when schema is not provided
51+
if (service && schema) {
52+
if (!schemas[schema]) {
53+
schemas[schema] = await fetchSchema(service, schema);
54+
}
55+
56+
const validation = ajv.validate(schemas[schema], value.payload);
57+
58+
if (!validation) {
59+
(ajv.errors || []).forEach(error => {
60+
errors.push(`Payload ${formatErrorDetails(error)}`);
61+
});
62+
}
63+
}
64+
65+
return errors;
66+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
describe('validation', () => {
2+
beforeAll(() => {
3+
window.fetch = jest.fn().mockImplementation(url => {
4+
return {
5+
json: () =>
6+
Promise.resolve({
7+
$id: url,
8+
}),
9+
};
10+
});
11+
});
12+
13+
it('should validate payload json', async () => {
14+
const validate = require('./validateTaskPayloadSchemas').default; // eslint-disable-line global-require
15+
16+
const errors = await validate('');
17+
18+
expect(errors).toBeDefined();
19+
expect(errors).toEqual([]);
20+
expect(window.fetch).toHaveBeenCalled();
21+
});
22+
23+
it('should format messages', () => {
24+
const { formatErrorDetails } = require('./validateTaskPayloadSchemas'); // eslint-disable-line global-require
25+
26+
expect(
27+
formatErrorDetails({
28+
message: 'Invalid',
29+
})
30+
).toEqual('Invalid');
31+
expect(
32+
formatErrorDetails({
33+
message: 'Invalid',
34+
keyword: 'type',
35+
instancePath: '/cmd/0',
36+
})
37+
).toEqual("Invalid '/cmd/0'");
38+
expect(
39+
formatErrorDetails({
40+
message: 'Invalid',
41+
keyword: 'additionalProperties',
42+
params: {
43+
additionalProperty: 'extra',
44+
},
45+
})
46+
).toEqual("Invalid 'extra'");
47+
});
48+
});

0 commit comments

Comments
 (0)