From 6c65966c1660e038c94a7ab45c0165b95c7ebb0f Mon Sep 17 00:00:00 2001 From: Jai Bhagat Date: Wed, 26 Jan 2022 11:30:06 -0500 Subject: [PATCH] refact: fix tests after contextual job page changes --- ui/app/routes/jobs/job/index.js | 4 +- .../parts/job-client-status-summary.hbs | 2 +- .../components/job-page/parts/summary.hbs | 25 +- ui/app/templates/components/job-subnav.hbs | 2 +- .../list-accordion/accordion-head.hbs | 9 +- ui/mirage/config.js | 537 +++++++++++------- ui/tests/acceptance/job-clients-test.js | 20 + ui/tests/helpers/module-for-job.js | 18 +- .../job-page/parts/children-test.js | 53 +- .../job-page/parts/task-groups-test.js | 1 + .../pages/components/job-client-status-bar.js | 5 + ui/tests/pages/jobs/detail.js | 18 +- ui/tests/unit/utils/job-client-status-test.js | 54 +- 13 files changed, 453 insertions(+), 295 deletions(-) diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index f5b7d7b3205..be195becc5f 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -32,7 +32,9 @@ export default class IndexRoute extends Route.extend(WithWatchers) { model.get('hasChildren') && this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), nodes: - model.get('hasClientStatus') && this.can.can('read client') && this.watchNodes.perform(), + model.get('hasClientStatus') && + this.can.can('read client') && + this.watchNodes.perform(), }); } diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index 6d368958747..589b8d0624e 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -1,6 +1,6 @@ {{#if this.job.hasClientStatus}} - +
{{#if a.item.hasChildren}} @@ -32,7 +35,10 @@ {{/if}} {{else}} - + {{/if}}
@@ -55,7 +61,10 @@ {{if (eq datum.value 0) "is-empty"}}" > - + {{/each}} @@ -83,10 +92,16 @@ @model={{this.job}} @query={{datum.legendLink.queryParams}} > - + {{else}} - + {{/if}} {{/each}} diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index 32050a42e4b..f9c11e8e6f6 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -31,7 +31,7 @@ Versions - {{#if this.job.supportsDeployments}} + {{#if @job.supportsDeployments}}
  • + \ No newline at end of file diff --git a/ui/mirage/config.js b/ui/mirage/config.js index dda16f67f62..b95244b545e 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -14,14 +14,14 @@ export function findLeader(schema) { export function filesForPath(allocFiles, filterPath) { return allocFiles.where( - file => + (file) => (!filterPath || file.path.startsWith(filterPath)) && file.path.length > filterPath.length && !file.path.substr(filterPath.length + 1).includes('/') ); } -export default function() { +export default function () { this.timing = 0; // delay for each request, automatically set to 0 during testing this.logging = window.location.search.includes('mirage-logging=true'); @@ -31,8 +31,8 @@ export default function() { const nomadIndices = {}; // used for tracking blocking queries const server = this; - const withBlockingSupport = function(fn) { - return function(schema, request) { + const withBlockingSupport = function (fn) { + return function (schema, request) { // Get the original response let { url } = request; url = url.replace(/index=\d+[&;]?/, ''); @@ -54,34 +54,44 @@ export default function() { this.get( '/jobs', - withBlockingSupport(function({ jobs }, { queryParams }) { + withBlockingSupport(function ({ jobs }, { queryParams }) { const json = this.serialize(jobs.all()); const namespace = queryParams.namespace || 'default'; return json - .filter(job => { + .filter((job) => { if (namespace === '*') return true; return namespace === 'default' ? !job.NamespaceID || job.NamespaceID === namespace : job.NamespaceID === namespace; }) - .map(job => filterKeys(job, 'TaskGroups', 'NamespaceID')); + .map((job) => filterKeys(job, 'TaskGroups', 'NamespaceID')); }) ); - this.post('/jobs', function(schema, req) { + this.post('/jobs', function (schema, req) { const body = JSON.parse(req.requestBody); - if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); + if (!body.Job) + return new Response( + 400, + {}, + 'Job is a required field on the request payload' + ); return okEmpty(); }); - this.post('/jobs/parse', function(schema, req) { + this.post('/jobs/parse', function (schema, req) { const body = JSON.parse(req.requestBody); if (!body.JobHCL) - return new Response(400, {}, 'JobHCL is a required field on the request payload'); - if (!body.Canonicalize) return new Response(400, {}, 'Expected Canonicalize to be true'); + return new Response( + 400, + {}, + 'JobHCL is a required field on the request payload' + ); + if (!body.Canonicalize) + return new Response(400, {}, 'Expected Canonicalize to be true'); // Parse the name out of the first real line of HCL to match IDs in the new job record // Regex expectation: @@ -96,13 +106,19 @@ export default function() { return new Response(200, {}, this.serialize(job)); }); - this.post('/job/:id/plan', function(schema, req) { + this.post('/job/:id/plan', function (schema, req) { const body = JSON.parse(req.requestBody); - if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); + if (!body.Job) + return new Response( + 400, + {}, + 'Job is a required field on the request payload' + ); if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true'); - const FailedTGAllocs = body.Job.Unschedulable && generateFailedTGAllocs(body.Job); + const FailedTGAllocs = + body.Job.Unschedulable && generateFailedTGAllocs(body.Job); return new Response( 200, @@ -113,13 +129,15 @@ export default function() { this.get( '/job/:id', - withBlockingSupport(function({ jobs }, { params, queryParams }) { - const job = jobs.all().models.find(job => { + withBlockingSupport(function ({ jobs }, { params, queryParams }) { + const job = jobs.all().models.find((job) => { const jobIsDefault = !job.namespaceId || job.namespaceId === 'default'; - const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default'; + const qpIsDefault = + !queryParams.namespace || queryParams.namespace === 'default'; return ( job.id === params.id && - (job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault)) + (job.namespaceId === queryParams.namespace || + (jobIsDefault && qpIsDefault)) ); }); @@ -127,47 +145,54 @@ export default function() { }) ); - this.post('/job/:id', function(schema, req) { + this.post('/job/:id', function (schema, req) { const body = JSON.parse(req.requestBody); - if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); + if (!body.Job) + return new Response( + 400, + {}, + 'Job is a required field on the request payload' + ); return okEmpty(); }); this.get( '/job/:id/summary', - withBlockingSupport(function({ jobSummaries }, { params }) { + withBlockingSupport(function ({ jobSummaries }, { params }) { return this.serialize(jobSummaries.findBy({ jobId: params.id })); }) ); - this.get('/job/:id/allocations', function({ allocations }, { params }) { + this.get('/job/:id/allocations', function ({ allocations }, { params }) { return this.serialize(allocations.where({ jobId: params.id })); }); - this.get('/job/:id/versions', function({ jobVersions }, { params }) { + this.get('/job/:id/versions', function ({ jobVersions }, { params }) { return this.serialize(jobVersions.where({ jobId: params.id })); }); - this.get('/job/:id/deployments', function({ deployments }, { params }) { + this.get('/job/:id/deployments', function ({ deployments }, { params }) { return this.serialize(deployments.where({ jobId: params.id })); }); - this.get('/job/:id/deployment', function({ deployments }, { params }) { + this.get('/job/:id/deployment', function ({ deployments }, { params }) { const deployment = deployments.where({ jobId: params.id }).models[0]; - return deployment ? this.serialize(deployment) : new Response(200, {}, 'null'); + return deployment + ? this.serialize(deployment) + : new Response(200, {}, 'null'); }); this.get( '/job/:id/scale', - withBlockingSupport(function({ jobScales }, { params }) { + withBlockingSupport(function ({ jobScales }, { params }) { const obj = jobScales.findBy({ jobId: params.id }); return this.serialize(jobScales.findBy({ jobId: params.id })); }) ); - this.post('/job/:id/periodic/force', function(schema, { params }) { + this.post('/job/:id/periodic/force', function (schema, { params }) { // Create the child job const parent = schema.jobs.find(params.id); @@ -182,7 +207,7 @@ export default function() { return okEmpty(); }); - this.post('/job/:id/dispatch', function(schema, { params }) { + this.post('/job/:id/dispatch', function (schema, { params }) { // Create the child job const parent = schema.jobs.find(params.id); @@ -203,7 +228,7 @@ export default function() { ); }); - this.post('/job/:id/revert', function({ jobs }, { requestBody }) { + this.post('/job/:id/revert', function ({ jobs }, { requestBody }) { const { JobID, JobVersion } = JSON.parse(requestBody); const job = jobs.find(JobID); job.version = JobVersion; @@ -212,11 +237,11 @@ export default function() { return okEmpty(); }); - this.post('/job/:id/scale', function({ jobs }, { params }) { + this.post('/job/:id/scale', function ({ jobs }, { params }) { return this.serialize(jobs.find(params.id)); }); - this.delete('/job/:id', function(schema, { params }) { + this.delete('/job/:id', function (schema, { params }) { const job = schema.jobs.find(params.id); job.update({ status: 'dead' }); return new Response(204, {}, ''); @@ -224,58 +249,70 @@ export default function() { this.get('/deployment/:id'); - this.post('/deployment/fail/:id', function() { + this.post('/deployment/fail/:id', function () { return new Response(204, {}, ''); }); - this.post('/deployment/promote/:id', function() { + this.post('/deployment/promote/:id', function () { return new Response(204, {}, ''); }); - this.get('/job/:id/evaluations', function({ evaluations }, { params }) { + this.get('/job/:id/evaluations', function ({ evaluations }, { params }) { return this.serialize(evaluations.where({ jobId: params.id })); }); this.get('/evaluation/:id'); - this.get('/deployment/allocations/:id', function(schema, { params }) { + this.get('/deployment/allocations/:id', function (schema, { params }) { const job = schema.jobs.find(schema.deployments.find(params.id).jobId); const allocations = schema.allocations.where({ jobId: job.id }); return this.serialize(allocations.slice(0, 3)); }); - this.get('/nodes', function({ nodes }, req) { + this.get('/nodes', function ({ nodes }, req) { // authorize user permissions - const { policyIds } = server.db.tokens.findBy({ + const token = server.db.tokens.findBy({ secretId: req.requestHeaders['X-Nomad-Token'], }); - const policies = server.db.policies.find(policyIds); - const hasReadPolicy = policies.find( - p => p.rulesJSON.Node?.Policy === 'read' || p.rulesJSON.Node?.Policy === 'write' - ); - if (hasReadPolicy) { - const json = this.serialize(nodes.all()); - return json; + + if (token) { + const { policyIds } = token; + const policies = server.db.policies.find(policyIds); + const hasReadPolicy = policies.find( + (p) => + p.rulesJSON.Node?.Policy === 'read' || + p.rulesJSON.Node?.Policy === 'write' + ); + if (hasReadPolicy) { + const json = this.serialize(nodes.all()); + return json; + } + return new Response(403, {}, 'Permissions have not be set-up.'); } - return new Response(403, {}, 'You broke everything!'); + + // TODO: Think about policy handling in Mirage set-up + return this.serialize(nodes.all()); }); this.get('/node/:id'); - this.get('/node/:id/allocations', function({ allocations }, { params }) { + this.get('/node/:id/allocations', function ({ allocations }, { params }) { return this.serialize(allocations.where({ nodeId: params.id })); }); - this.post('/node/:id/eligibility', function({ nodes }, { params, requestBody }) { - const body = JSON.parse(requestBody); - const node = nodes.find(params.id); + this.post( + '/node/:id/eligibility', + function ({ nodes }, { params, requestBody }) { + const body = JSON.parse(requestBody); + const node = nodes.find(params.id); - node.update({ schedulingEligibility: body.Elibility === 'eligible' }); - return this.serialize(node); - }); + node.update({ schedulingEligibility: body.Elibility === 'eligible' }); + return this.serialize(node); + } + ); - this.post('/node/:id/drain', function({ nodes }, { params }) { + this.post('/node/:id/drain', function ({ nodes }, { params }) { return this.serialize(nodes.find(params.id)); }); @@ -283,20 +320,20 @@ export default function() { this.get('/allocation/:id'); - this.post('/allocation/:id/stop', function() { + this.post('/allocation/:id/stop', function () { return new Response(204, {}, ''); }); this.get( '/volumes', - withBlockingSupport(function({ csiVolumes }, { queryParams }) { + withBlockingSupport(function ({ csiVolumes }, { queryParams }) { if (queryParams.type !== 'csi') { return new Response(200, {}, '[]'); } const json = this.serialize(csiVolumes.all()); const namespace = queryParams.namespace || 'default'; - return json.filter(volume => { + return json.filter((volume) => { if (namespace === '*') return true; return namespace === 'default' ? !volume.NamespaceID || volume.NamespaceID === namespace @@ -307,18 +344,21 @@ export default function() { this.get( '/volume/:id', - withBlockingSupport(function({ csiVolumes }, { params, queryParams }) { + withBlockingSupport(function ({ csiVolumes }, { params, queryParams }) { if (!params.id.startsWith('csi/')) { return new Response(404, {}, null); } const id = params.id.replace(/^csi\//, ''); - const volume = csiVolumes.all().models.find(volume => { - const volumeIsDefault = !volume.namespaceId || volume.namespaceId === 'default'; - const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default'; + const volume = csiVolumes.all().models.find((volume) => { + const volumeIsDefault = + !volume.namespaceId || volume.namespaceId === 'default'; + const qpIsDefault = + !queryParams.namespace || queryParams.namespace === 'default'; return ( volume.id === id && - (volume.namespaceId === queryParams.namespace || (volumeIsDefault && qpIsDefault)) + (volume.namespaceId === queryParams.namespace || + (volumeIsDefault && qpIsDefault)) ); }); @@ -326,7 +366,7 @@ export default function() { }) ); - this.get('/plugins', function({ csiPlugins }, { queryParams }) { + this.get('/plugins', function ({ csiPlugins }, { queryParams }) { if (queryParams.type !== 'csi') { return new Response(200, {}, '[]'); } @@ -334,7 +374,7 @@ export default function() { return this.serialize(csiPlugins.all()); }); - this.get('/plugin/:id', function({ csiPlugins }, { params }) { + this.get('/plugin/:id', function ({ csiPlugins }, { params }) { if (!params.id.startsWith('csi/')) { return new Response(404, {}, null); } @@ -349,7 +389,7 @@ export default function() { return this.serialize(volume); }); - this.get('/namespaces', function({ namespaces }) { + this.get('/namespaces', function ({ namespaces }) { const records = namespaces.all(); if (records.length) { @@ -359,23 +399,25 @@ export default function() { return this.serialize([{ Name: 'default' }]); }); - this.get('/namespace/:id', function({ namespaces }, { params }) { + this.get('/namespace/:id', function ({ namespaces }, { params }) { return this.serialize(namespaces.find(params.id)); }); - this.get('/agent/members', function({ agents, regions }) { + this.get('/agent/members', function ({ agents, regions }) { const firstRegion = regions.first(); return { ServerRegion: firstRegion ? firstRegion.id : null, - Members: this.serialize(agents.all()).map(({ member }) => ({ ...member })), + Members: this.serialize(agents.all()).map(({ member }) => ({ + ...member, + })), }; }); - this.get('/agent/self', function({ agents }) { + this.get('/agent/self', function ({ agents }) { return agents.first(); }); - this.get('/agent/monitor', function({ agents, nodes }, { queryParams }) { + this.get('/agent/monitor', function ({ agents, nodes }, { queryParams }) { const serverId = queryParams.server_id; const clientId = queryParams.client_id; @@ -393,11 +435,11 @@ export default function() { return logEncode(logFrames, logFrames.length - 1); }); - this.get('/status/leader', function(schema) { + this.get('/status/leader', function (schema) { return JSON.stringify(findLeader(schema)); }); - this.get('/acl/token/self', function({ tokens }, req) { + this.get('/acl/token/self', function ({ tokens }, req) { const secret = req.requestHeaders['X-Nomad-Token']; const tokenForSecret = tokens.findBy({ secretId: secret }); @@ -410,14 +452,17 @@ export default function() { return new Response(400, {}, null); }); - this.get('/acl/token/:id', function({ tokens }, req) { + this.get('/acl/token/:id', function ({ tokens }, req) { const token = tokens.find(req.params.id); const secret = req.requestHeaders['X-Nomad-Token']; const tokenForSecret = tokens.findBy({ secretId: secret }); // Return the token only if the request header matches the token // or the token is of type management - if (token.secretId === secret || (tokenForSecret && tokenForSecret.type === 'management')) { + if ( + token.secretId === secret || + (tokenForSecret && tokenForSecret.type === 'management') + ) { return this.serialize(token); } @@ -425,23 +470,26 @@ export default function() { return new Response(403, {}, null); }); - this.post('/acl/token/onetime/exchange', function({ tokens }, { requestBody }) { - const { OneTimeSecretID } = JSON.parse(requestBody); + this.post( + '/acl/token/onetime/exchange', + function ({ tokens }, { requestBody }) { + const { OneTimeSecretID } = JSON.parse(requestBody); - const tokenForSecret = tokens.findBy({ oneTimeSecret: OneTimeSecretID }); + const tokenForSecret = tokens.findBy({ oneTimeSecret: OneTimeSecretID }); - // Return the token if it exists - if (tokenForSecret) { - return { - Token: this.serialize(tokenForSecret), - }; - } + // Return the token if it exists + if (tokenForSecret) { + return { + Token: this.serialize(tokenForSecret), + }; + } - // Forbidden error if it doesn't - return new Response(403, {}, null); - }); + // Forbidden error if it doesn't + return new Response(403, {}, null); + } + ); - this.get('/acl/policy/:id', function({ policies, tokens }, req) { + this.get('/acl/policy/:id', function ({ policies, tokens }, req) { const policy = policies.find(req.params.id); const secret = req.requestHeaders['X-Nomad-Token']; const tokenForSecret = tokens.findBy({ secretId: secret }); @@ -459,7 +507,8 @@ export default function() { // is of type management if ( tokenForSecret && - (tokenForSecret.policies.includes(policy) || tokenForSecret.type === 'management') + (tokenForSecret.policies.includes(policy) || + tokenForSecret.type === 'management') ) { return this.serialize(policy); } @@ -468,11 +517,11 @@ export default function() { return new Response(403, {}, null); }); - this.get('/regions', function({ regions }) { + this.get('/regions', function ({ regions }) { return this.serialize(regions.all()); }); - this.get('/operator/license', function({ features }) { + this.get('/operator/license', function ({ features }) { const records = features.all(); if (records.length) { @@ -486,13 +535,18 @@ export default function() { return new Response(501, {}, null); }); - const clientAllocationStatsHandler = function({ clientAllocationStats }, { params }) { + const clientAllocationStatsHandler = function ( + { clientAllocationStats }, + { params } + ) { return this.serialize(clientAllocationStats.find(params.id)); }; - const clientAllocationLog = function(server, { params, queryParams }) { + const clientAllocationLog = function (server, { params, queryParams }) { const allocation = server.allocations.find(params.allocation_id); - const tasks = allocation.taskStateIds.map(id => server.taskStates.find(id)); + const tasks = allocation.taskStateIds.map((id) => + server.taskStates.find(id) + ); if (!tasks.mapBy('name').includes(queryParams.task)) { return new Response(400, {}, 'must include task name'); @@ -505,14 +559,24 @@ export default function() { return logEncode(logFrames, logFrames.length - 1); }; - const clientAllocationFSLsHandler = function({ allocFiles }, { queryParams: { path } }) { - const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; + const clientAllocationFSLsHandler = function ( + { allocFiles }, + { queryParams: { path } } + ) { + const filterPath = path.endsWith('/') + ? path.substr(0, path.length - 1) + : path; const files = filesForPath(allocFiles, filterPath); return this.serialize(files); }; - const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams: { path } }) { - const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; + const clientAllocationFSStatHandler = function ( + { allocFiles }, + { queryParams: { path } } + ) { + const filterPath = path.endsWith('/') + ? path.substr(0, path.length - 1) + : path; // Root path if (!filterPath) { @@ -527,14 +591,20 @@ export default function() { return this.serialize(file); }; - const clientAllocationCatHandler = function({ allocFiles }, { queryParams }) { + const clientAllocationCatHandler = function ( + { allocFiles }, + { queryParams } + ) { const [file, err] = fileOrError(allocFiles, queryParams.path); if (err) return err; return file.body; }; - const clientAllocationStreamHandler = function({ allocFiles }, { queryParams }) { + const clientAllocationStreamHandler = function ( + { allocFiles }, + { queryParams } + ) { const [file, err] = fileOrError(allocFiles, queryParams.path); if (err) return err; @@ -543,14 +613,21 @@ export default function() { return file.body; }; - const clientAllocationReadAtHandler = function({ allocFiles }, { queryParams }) { + const clientAllocationReadAtHandler = function ( + { allocFiles }, + { queryParams } + ) { const [file, err] = fileOrError(allocFiles, queryParams.path); if (err) return err; return file.body.substr(queryParams.offset || 0, queryParams.limit); }; - const fileOrError = function(allocFiles, path, message = 'Operation not allowed on a directory') { + const fileOrError = function ( + allocFiles, + path, + message = 'Operation not allowed on a directory' + ) { // Root path if (path === '/') { return [null, new Response(400, {}, message)]; @@ -565,7 +642,7 @@ export default function() { }; // Client requests are available on the server and the client - this.put('/client/allocation/:id/restart', function() { + this.put('/client/allocation/:id/restart', function () { return new Response(204, {}, ''); }); @@ -578,13 +655,14 @@ export default function() { this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler); this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler); - this.get('/client/stats', function({ clientStats }, { queryParams }) { + this.get('/client/stats', function ({ clientStats }, { queryParams }) { const seed = faker.random.number(10); if (seed >= 8) { const stats = clientStats.find(queryParams.node_id); stats.update({ timestamp: Date.now() * 1000000, - CPUTicksConsumed: stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }), + CPUTicksConsumed: + stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }), }); return this.serialize(stats); } else { @@ -594,134 +672,172 @@ export default function() { // TODO: in the future, this hack may be replaceable with dynamic host name // support in pretender: https://github.com/pretenderjs/pretender/issues/210 - HOSTS.forEach(host => { - this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler); - this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog); + HOSTS.forEach((host) => { + this.get( + `http://${host}/v1/client/allocation/:id/stats`, + clientAllocationStatsHandler + ); + this.get( + `http://${host}/v1/client/fs/logs/:allocation_id`, + clientAllocationLog + ); - this.get(`http://${host}/v1/client/fs/ls/:allocation_id`, clientAllocationFSLsHandler); - this.get(`http://${host}/v1/client/stat/ls/:allocation_id`, clientAllocationFSStatHandler); - this.get(`http://${host}/v1/client/fs/cat/:allocation_id`, clientAllocationCatHandler); - this.get(`http://${host}/v1/client/fs/stream/:allocation_id`, clientAllocationStreamHandler); - this.get(`http://${host}/v1/client/fs/readat/:allocation_id`, clientAllocationReadAtHandler); + this.get( + `http://${host}/v1/client/fs/ls/:allocation_id`, + clientAllocationFSLsHandler + ); + this.get( + `http://${host}/v1/client/stat/ls/:allocation_id`, + clientAllocationFSStatHandler + ); + this.get( + `http://${host}/v1/client/fs/cat/:allocation_id`, + clientAllocationCatHandler + ); + this.get( + `http://${host}/v1/client/fs/stream/:allocation_id`, + clientAllocationStreamHandler + ); + this.get( + `http://${host}/v1/client/fs/readat/:allocation_id`, + clientAllocationReadAtHandler + ); - this.get(`http://${host}/v1/client/stats`, function({ clientStats }) { + this.get(`http://${host}/v1/client/stats`, function ({ clientStats }) { return this.serialize(clientStats.find(host)); }); }); - this.post('/search/fuzzy', function( - { allocations, jobs, nodes, taskGroups, csiPlugins }, - { requestBody } - ) { - const { Text } = JSON.parse(requestBody); - - const matchedAllocs = allocations.where(allocation => allocation.name.includes(Text)); - const matchedGroups = taskGroups.where(taskGroup => taskGroup.name.includes(Text)); - const matchedJobs = jobs.where(job => job.name.includes(Text)); - const matchedNodes = nodes.where(node => node.name.includes(Text)); - const matchedPlugins = csiPlugins.where(plugin => plugin.id.includes(Text)); - - const transformedAllocs = matchedAllocs.models.map(alloc => ({ - ID: alloc.name, - Scope: [alloc.namespace || 'default', alloc.id], - })); - - const transformedGroups = matchedGroups.models.map(group => ({ - ID: group.name, - Scope: [group.job.namespace, group.job.id], - })); - - const transformedJobs = matchedJobs.models.map(job => ({ - ID: job.name, - Scope: [job.namespace || 'default', job.id], - })); - - const transformedNodes = matchedNodes.models.map(node => ({ - ID: node.name, - Scope: [node.id], - })); - - const transformedPlugins = matchedPlugins.models.map(plugin => ({ - ID: plugin.id, - })); - - const truncatedAllocs = transformedAllocs.slice(0, 20); - const truncatedGroups = transformedGroups.slice(0, 20); - const truncatedJobs = transformedJobs.slice(0, 20); - const truncatedNodes = transformedNodes.slice(0, 20); - const truncatedPlugins = transformedPlugins.slice(0, 20); + this.post( + '/search/fuzzy', + function ( + { allocations, jobs, nodes, taskGroups, csiPlugins }, + { requestBody } + ) { + const { Text } = JSON.parse(requestBody); + + const matchedAllocs = allocations.where((allocation) => + allocation.name.includes(Text) + ); + const matchedGroups = taskGroups.where((taskGroup) => + taskGroup.name.includes(Text) + ); + const matchedJobs = jobs.where((job) => job.name.includes(Text)); + const matchedNodes = nodes.where((node) => node.name.includes(Text)); + const matchedPlugins = csiPlugins.where((plugin) => + plugin.id.includes(Text) + ); + + const transformedAllocs = matchedAllocs.models.map((alloc) => ({ + ID: alloc.name, + Scope: [alloc.namespace || 'default', alloc.id], + })); + + const transformedGroups = matchedGroups.models.map((group) => ({ + ID: group.name, + Scope: [group.job.namespace, group.job.id], + })); + + const transformedJobs = matchedJobs.models.map((job) => ({ + ID: job.name, + Scope: [job.namespace || 'default', job.id], + })); + + const transformedNodes = matchedNodes.models.map((node) => ({ + ID: node.name, + Scope: [node.id], + })); + + const transformedPlugins = matchedPlugins.models.map((plugin) => ({ + ID: plugin.id, + })); + + const truncatedAllocs = transformedAllocs.slice(0, 20); + const truncatedGroups = transformedGroups.slice(0, 20); + const truncatedJobs = transformedJobs.slice(0, 20); + const truncatedNodes = transformedNodes.slice(0, 20); + const truncatedPlugins = transformedPlugins.slice(0, 20); - return { - Matches: { - allocs: truncatedAllocs, - groups: truncatedGroups, - jobs: truncatedJobs, - nodes: truncatedNodes, - plugins: truncatedPlugins, - }, - Truncations: { - allocs: truncatedAllocs.length < truncatedAllocs.length, - groups: truncatedGroups.length < transformedGroups.length, - jobs: truncatedJobs.length < transformedJobs.length, - nodes: truncatedNodes.length < transformedNodes.length, - plugins: truncatedPlugins.length < transformedPlugins.length, - }, - }; - }); + return { + Matches: { + allocs: truncatedAllocs, + groups: truncatedGroups, + jobs: truncatedJobs, + nodes: truncatedNodes, + plugins: truncatedPlugins, + }, + Truncations: { + allocs: truncatedAllocs.length < truncatedAllocs.length, + groups: truncatedGroups.length < transformedGroups.length, + jobs: truncatedJobs.length < transformedJobs.length, + nodes: truncatedNodes.length < transformedNodes.length, + plugins: truncatedPlugins.length < transformedPlugins.length, + }, + }; + } + ); - this.get('/recommendations', function( - { jobs, namespaces, recommendations }, - { queryParams: { job: id, namespace } } - ) { - if (id) { - if (!namespaces.all().length) { - namespace = null; - } + this.get( + '/recommendations', + function ( + { jobs, namespaces, recommendations }, + { queryParams: { job: id, namespace } } + ) { + if (id) { + if (!namespaces.all().length) { + namespace = null; + } - const job = jobs.findBy({ id, namespace }); + const job = jobs.findBy({ id, namespace }); - if (!job) { - return []; - } + if (!job) { + return []; + } - const taskGroups = job.taskGroups.models; + const taskGroups = job.taskGroups.models; - const tasks = taskGroups.reduce((tasks, taskGroup) => { - return tasks.concat(taskGroup.tasks.models); - }, []); + const tasks = taskGroups.reduce((tasks, taskGroup) => { + return tasks.concat(taskGroup.tasks.models); + }, []); - const recommendationIds = tasks.reduce((recommendationIds, task) => { - return recommendationIds.concat(task.recommendations.models.mapBy('id')); - }, []); + const recommendationIds = tasks.reduce((recommendationIds, task) => { + return recommendationIds.concat( + task.recommendations.models.mapBy('id') + ); + }, []); - return recommendations.find(recommendationIds); - } else { - return recommendations.all(); + return recommendations.find(recommendationIds); + } else { + return recommendations.all(); + } } - }); + ); - this.post('/recommendations/apply', function({ recommendations }, { requestBody }) { - const { Apply, Dismiss } = JSON.parse(requestBody); + this.post( + '/recommendations/apply', + function ({ recommendations }, { requestBody }) { + const { Apply, Dismiss } = JSON.parse(requestBody); - Apply.concat(Dismiss).forEach(id => { - const recommendation = recommendations.find(id); - const task = recommendation.task; + Apply.concat(Dismiss).forEach((id) => { + const recommendation = recommendations.find(id); + const task = recommendation.task; - if (Apply.includes(id)) { - task.resources[recommendation.resource] = recommendation.value; - } - recommendation.destroy(); - task.save(); - }); + if (Apply.includes(id)) { + task.resources[recommendation.resource] = recommendation.value; + } + recommendation.destroy(); + task.save(); + }); - return {}; - }); + return {}; + } + ); } function filterKeys(object, ...keys) { const clone = copy(object, true); - keys.forEach(key => { + keys.forEach((key) => { delete clone[key]; }); @@ -738,7 +854,8 @@ function generateFailedTGAllocs(job, taskGroups) { const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name'); let tgNames = ['tg-one', 'tg-two']; - if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec; + if (taskGroupsFromSpec && taskGroupsFromSpec.length) + tgNames = taskGroupsFromSpec; if (taskGroups && taskGroups.length) tgNames = taskGroups; return tgNames.reduce((hash, tgName) => { diff --git a/ui/tests/acceptance/job-clients-test.js b/ui/tests/acceptance/job-clients-test.js index 4af17e15dc4..4f1ac8bde51 100644 --- a/ui/tests/acceptance/job-clients-test.js +++ b/ui/tests/acceptance/job-clients-test.js @@ -27,6 +27,16 @@ module('Acceptance | job clients', function (hooks) { setupMirage(hooks); hooks.beforeEach(function () { + setPolicy({ + id: 'node-read', + name: 'node-read', + rulesJSON: { + Node: { + Policy: 'read', + }, + }, + }); + clients = server.createList('node', 12, { datacenter: 'dc1', status: 'ready', @@ -217,3 +227,13 @@ module('Acceptance | job clients', function (hooks) { // TODO: add facet tests for actual list filtering } }); + +function setPolicy(policy) { + const { id: policyId } = server.create('policy', policy); + const clientToken = server.create('token', { type: 'client' }); + clientToken.policyIds = [policyId]; + clientToken.save(); + + window.localStorage.clear(); + window.localStorage.nomadTokenSecret = clientToken.secretId; +} diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 81dc7fba507..58d078121b3 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -1,6 +1,11 @@ /* eslint-disable qunit/require-expect */ /* eslint-disable qunit/no-conditional-assertions */ -import { currentRouteName, currentURL, visit } from '@ember/test-helpers'; +import { + click, + currentRouteName, + currentURL, + visit, +} from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -37,6 +42,11 @@ export default function moduleForJob( } else { await JobDetail.visit({ id: job.id, namespace: job.namespace }); } + + const hasClientStatus = ['system', 'sysbatch'].includes(job.type); + if (context === 'allocations' && hasClientStatus) { + await click("[data-test-accordion-summary-chart='allocation-status']"); + } }); test('visiting /jobs/:job_id', async function (assert) { @@ -248,14 +258,14 @@ export function moduleForJobWithClientStatus( test('job status summary is shown in the overview', async function (assert) { assert.ok( - JobDetail.jobClientStatusSummary.isPresent, + JobDetail.jobClientStatusSummary.statusBar.isPresent, 'Summary bar is displayed in the Job Status in Client summary section' ); }); test('clicking legend item navigates to a pre-filtered clients table', async function (assert) { const legendItem = - JobDetail.jobClientStatusSummary.legend.clickableItems[0]; + JobDetail.jobClientStatusSummary.statusBar.legend.clickableItems[0]; const status = legendItem.label; await legendItem.click(); @@ -273,7 +283,7 @@ export function moduleForJobWithClientStatus( }); test('clicking in a slice takes you to a pre-filtered clients table', async function (assert) { - const slice = JobDetail.jobClientStatusSummary.slices[0]; + const slice = JobDetail.jobClientStatusSummary.statusBar.slices[0]; const status = slice.label; await slice.click(); diff --git a/ui/tests/integration/components/job-page/parts/children-test.js b/ui/tests/integration/components/job-page/parts/children-test.js index 3c7bb98c92c..274b9ab5a5e 100644 --- a/ui/tests/integration/components/job-page/parts/children-test.js +++ b/ui/tests/integration/components/job-page/parts/children-test.js @@ -1,7 +1,6 @@ import { assign } from '@ember/polyfills'; import hbs from 'htmlbars-inline-precompile'; -import { findAll, find, click, render } from '@ember/test-helpers'; -import sinon from 'sinon'; +import { findAll, find, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; @@ -29,7 +28,6 @@ module('Integration | Component | job-page/parts/children', function (hooks) { sortProperty: 'name', sortDescending: true, currentPage: 1, - gotoJob: () => {}, }, options ); @@ -87,7 +85,7 @@ module('Integration | Component | job-page/parts/children', function (hooks) { @sortProperty={{sortProperty}} @sortDescending={{sortDescending}} @currentPage={{currentPage}} - @gotoJob={{gotoJob}} /> + /> `); const childrenCount = parent.get('children.length'); @@ -102,11 +100,12 @@ module('Integration | Component | job-page/parts/children', function (hooks) { ); assert.ok(find('.pagination-next'), 'Next button is rendered'); - assert.ok( - new RegExp(`1.10.+?${childrenCount}`).test( - find('.pagination-numbers').textContent.trim() - ) - ); + assert + .dom('.pagination-numbers') + .includesText( + '1 – 10 of 11', + 'Formats pagination to follow formula `startingIdx - endingIdx of totalTableCount' + ); await componentA11yAudit(this.element, assert); }); @@ -156,40 +155,4 @@ module('Integration | Component | job-page/parts/children', function (hooks) { ); }); }); - - test('gotoJob is called when a job row is clicked', async function (assert) { - const gotoJobSpy = sinon.spy(); - - this.server.create('job', 'periodic', { - id: 'parent', - childrenCount: 1, - createAllocations: false, - }); - - await this.store.findAll('job'); - - const parent = this.store.peekAll('job').findBy('plainId', 'parent'); - - this.setProperties( - props(parent, { - gotoJob: gotoJobSpy, - }) - ); - - await render(hbs` - - `); - - await click('tr.job-row'); - - assert.ok( - gotoJobSpy.withArgs(parent.get('children.firstObject')).calledOnce, - 'Clicking the job row calls the gotoJob action' - ); - }); }); diff --git a/ui/tests/integration/components/job-page/parts/task-groups-test.js b/ui/tests/integration/components/job-page/parts/task-groups-test.js index 5d6836ec3df..a5a44f5bc0d 100644 --- a/ui/tests/integration/components/job-page/parts/task-groups-test.js +++ b/ui/tests/integration/components/job-page/parts/task-groups-test.js @@ -86,6 +86,7 @@ module( @job={{this.job}} @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} + /> `); const taskGroupRow = find('[data-test-task-group]'); diff --git a/ui/tests/pages/components/job-client-status-bar.js b/ui/tests/pages/components/job-client-status-bar.js index 6172259cc2a..83b7329022b 100644 --- a/ui/tests/pages/components/job-client-status-bar.js +++ b/ui/tests/pages/components/job-client-status-bar.js @@ -8,6 +8,11 @@ export default (scope) => ({ click: clickable(), }), + expand: { + scope: '[data-test-accordion-toggle]', + click: clickable(), + }, + legend: { scope: '.legend', diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js index 0cf45eb2626..9c07feaa727 100644 --- a/ui/tests/pages/jobs/detail.js +++ b/ui/tests/pages/jobs/detail.js @@ -75,16 +75,22 @@ export default create({ return this.packStats.toArray().findBy('id', id); }, - jobClientStatusSummary: jobClientStatusBar( - '[data-test-job-client-status-bar]' - ), - childrenSummary: isPresent( + jobClientStatusSummary: { + scope: '[data-test-job-client-summary]', + statusBar: jobClientStatusBar('[data-test-job-client-status-bar]'), + toggle: { + scope: '[data-test-accordion-head] [data-test-accordion-toggle]', + click: clickable(), + isDisabled: attribute('disabled'), + tooltip: attribute('aria-label'), + }, + }, + childrenSummary: jobClientStatusBar( '[data-test-job-summary] [data-test-children-status-bar]' ), - allocationsSummary: isPresent( + allocationsSummary: jobClientStatusBar( '[data-test-job-summary] [data-test-allocation-status-bar]' ), - ...allocations(), viewAllAllocations: text('[data-test-view-all-allocations]'), diff --git a/ui/tests/unit/utils/job-client-status-test.js b/ui/tests/unit/utils/job-client-status-test.js index 0c3d88465ab..e625a4f5730 100644 --- a/ui/tests/unit/utils/job-client-status-test.js +++ b/ui/tests/unit/utils/job-client-status-test.js @@ -35,6 +35,22 @@ class NodeMock { } } +class AllocationMock { + constructor(node, clientStatus) { + this.node = node; + this.clientStatus = clientStatus; + } + + belongsTo() { + const self = this; + return { + id() { + return self.node.id; + }, + }; + } +} + module('Unit | Util | JobClientStatus', function () { test('it handles the case where all nodes are running', async function (assert) { const node = new NodeMock('node-1', 'dc1'); @@ -42,7 +58,7 @@ module('Unit | Util | JobClientStatus', function () { const job = { datacenters: ['dc1'], status: 'running', - allocations: [{ node, clientStatus: 'running' }], + allocations: [new AllocationMock(node, 'running')], taskGroups: [{}], }; const expected = { @@ -75,9 +91,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'running' }, - { node, clientStatus: 'failed' }, - { node, clientStatus: 'running' }, + new AllocationMock(node, 'running'), + new AllocationMock(node, 'failed'), + new AllocationMock(node, 'running'), ], taskGroups: [{}, {}, {}], }; @@ -111,9 +127,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'lost' }, - { node, clientStatus: 'lost' }, - { node, clientStatus: 'lost' }, + new AllocationMock(node, 'lost'), + new AllocationMock(node, 'lost'), + new AllocationMock(node, 'lost'), ], taskGroups: [{}, {}, {}], }; @@ -147,9 +163,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'failed' }, - { node, clientStatus: 'failed' }, - { node, clientStatus: 'failed' }, + new AllocationMock(node, 'failed'), + new AllocationMock(node, 'failed'), + new AllocationMock(node, 'failed'), ], taskGroups: [{}, {}, {}], }; @@ -183,9 +199,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node, clientStatus: 'running' }, - { node, clientStatus: 'running' }, - { node, clientStatus: 'running' }, + new AllocationMock(node, 'running'), + new AllocationMock(node, 'running'), + new AllocationMock(node, 'running'), ], taskGroups: [{}, {}, {}, {}], }; @@ -251,9 +267,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'pending', allocations: [ - { node, clientStatus: 'starting' }, - { node, clientStatus: 'starting' }, - { node, clientStatus: 'starting' }, + new AllocationMock(node, 'starting'), + new AllocationMock(node, 'starting'), + new AllocationMock(node, 'starting'), ], taskGroups: [{}, {}, {}, {}], }; @@ -288,9 +304,9 @@ module('Unit | Util | JobClientStatus', function () { datacenters: ['dc1'], status: 'running', allocations: [ - { node: node1, clientStatus: 'running' }, - { node: node2, clientStatus: 'failed' }, - { node: node1, clientStatus: 'running' }, + new AllocationMock(node1, 'running'), + new AllocationMock(node2, 'failed'), + new AllocationMock(node1, 'running'), ], taskGroups: [{}, {}], };