2017-09-19 14:47:10 +00:00
|
|
|
import Ember from 'ember';
|
2017-10-10 18:23:10 +00:00
|
|
|
import Response from 'ember-cli-mirage/response';
|
2017-09-19 14:47:10 +00:00
|
|
|
import { HOSTS } from './common';
|
2017-11-16 02:12:16 +00:00
|
|
|
import { logFrames, logEncode } from './data/logs';
|
2018-08-15 23:59:42 +00:00
|
|
|
import { generateDiff } from './factories/job-version';
|
2018-08-20 22:04:33 +00:00
|
|
|
import { generateTaskGroupFailures } from './factories/evaluation';
|
2019-03-26 04:55:06 +00:00
|
|
|
import { copy } from 'ember-copy';
|
2021-05-13 17:29:51 +00:00
|
|
|
import formatHost from 'nomad-ui/utils/format-host';
|
2022-08-15 21:24:34 +00:00
|
|
|
import faker from 'nomad-ui/mirage/faker';
|
2017-09-19 14:47:10 +00:00
|
|
|
|
|
|
|
export function findLeader(schema) {
|
|
|
|
const agent = schema.agents.first();
|
2021-07-11 19:50:42 +00:00
|
|
|
return formatHost(agent.member.Address, agent.member.Tags.port);
|
2017-09-19 14:47:10 +00:00
|
|
|
}
|
|
|
|
|
2019-07-31 08:41:00 +00:00
|
|
|
export function filesForPath(allocFiles, filterPath) {
|
|
|
|
return allocFiles.where(
|
2022-01-26 16:30:06 +00:00
|
|
|
(file) =>
|
2019-07-31 08:41:00 +00:00
|
|
|
(!filterPath || file.path.startsWith(filterPath)) &&
|
|
|
|
file.path.length > filterPath.length &&
|
|
|
|
!file.path.substr(filterPath.length + 1).includes('/')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
export default function () {
|
2017-10-10 16:36:36 +00:00
|
|
|
this.timing = 0; // delay for each request, automatically set to 0 during testing
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2019-08-20 13:36:08 +00:00
|
|
|
this.logging = window.location.search.includes('mirage-logging=true');
|
|
|
|
|
2017-09-19 14:47:10 +00:00
|
|
|
this.namespace = 'v1';
|
2017-12-12 21:28:40 +00:00
|
|
|
this.trackRequests = Ember.testing;
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2018-02-17 02:58:19 +00:00
|
|
|
const nomadIndices = {}; // used for tracking blocking queries
|
|
|
|
const server = this;
|
2022-01-26 16:30:06 +00:00
|
|
|
const withBlockingSupport = function (fn) {
|
|
|
|
return function (schema, request) {
|
2018-02-17 02:58:19 +00:00
|
|
|
// Get the original response
|
|
|
|
let { url } = request;
|
|
|
|
url = url.replace(/index=\d+[&;]?/, '');
|
|
|
|
const response = fn.apply(this, arguments);
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2018-03-11 17:39:47 +00:00
|
|
|
// Get and increment the appropriate index
|
2018-03-21 20:28:56 +00:00
|
|
|
nomadIndices[url] || (nomadIndices[url] = 2);
|
2018-02-17 02:58:19 +00:00
|
|
|
const index = nomadIndices[url];
|
|
|
|
nomadIndices[url]++;
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2018-02-17 02:58:19 +00:00
|
|
|
// Annotate the response with the index
|
|
|
|
if (response instanceof Response) {
|
2020-05-21 00:46:29 +00:00
|
|
|
response.headers['x-nomad-index'] = index;
|
2018-02-17 02:58:19 +00:00
|
|
|
return response;
|
|
|
|
}
|
2018-02-22 21:21:32 +00:00
|
|
|
return new Response(200, { 'x-nomad-index': index }, response);
|
2018-02-17 02:58:19 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
this.get(
|
|
|
|
'/jobs',
|
2022-01-26 16:30:06 +00:00
|
|
|
withBlockingSupport(function ({ jobs }, { queryParams }) {
|
2018-02-17 02:58:19 +00:00
|
|
|
const json = this.serialize(jobs.all());
|
|
|
|
const namespace = queryParams.namespace || 'default';
|
|
|
|
return json
|
2022-01-26 16:30:06 +00:00
|
|
|
.filter((job) => {
|
2021-04-29 20:00:59 +00:00
|
|
|
if (namespace === '*') return true;
|
|
|
|
return namespace === 'default'
|
2019-04-02 01:30:44 +00:00
|
|
|
? !job.NamespaceID || job.NamespaceID === namespace
|
2021-04-29 20:00:59 +00:00
|
|
|
: job.NamespaceID === namespace;
|
|
|
|
})
|
2022-01-26 16:30:06 +00:00
|
|
|
.map((job) => filterKeys(job, 'TaskGroups', 'NamespaceID'));
|
2018-02-17 02:58:19 +00:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/jobs', function (schema, req) {
|
2018-08-15 23:59:42 +00:00
|
|
|
const body = JSON.parse(req.requestBody);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
if (!body.Job)
|
|
|
|
return new Response(
|
|
|
|
400,
|
|
|
|
{},
|
|
|
|
'Job is a required field on the request payload'
|
|
|
|
);
|
2018-08-15 23:59:42 +00:00
|
|
|
|
|
|
|
return okEmpty();
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/jobs/parse', function (schema, req) {
|
2018-08-15 23:59:42 +00:00
|
|
|
const body = JSON.parse(req.requestBody);
|
|
|
|
|
|
|
|
if (!body.JobHCL)
|
2022-01-26 16:30:06 +00:00
|
|
|
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');
|
2018-08-15 23:59:42 +00:00
|
|
|
|
2018-08-17 00:21:44 +00:00
|
|
|
// Parse the name out of the first real line of HCL to match IDs in the new job record
|
|
|
|
// Regex expectation:
|
|
|
|
// in: job "job-name" {
|
|
|
|
// out: job-name
|
|
|
|
const nameFromHCLBlock = /.+?"(.+?)"/;
|
|
|
|
const jobName = body.JobHCL.trim()
|
|
|
|
.split('\n')[0]
|
|
|
|
.match(nameFromHCLBlock)[1];
|
|
|
|
|
|
|
|
const job = server.create('job', { id: jobName });
|
|
|
|
return new Response(200, {}, this.serialize(job));
|
2018-08-15 23:59:42 +00:00
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/job/:id/plan', function (schema, req) {
|
2018-08-15 23:59:42 +00:00
|
|
|
const body = JSON.parse(req.requestBody);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
if (!body.Job)
|
|
|
|
return new Response(
|
|
|
|
400,
|
|
|
|
{},
|
|
|
|
'Job is a required field on the request payload'
|
|
|
|
);
|
2018-08-15 23:59:42 +00:00
|
|
|
if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true');
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const FailedTGAllocs =
|
|
|
|
body.Job.Unschedulable && generateFailedTGAllocs(body.Job);
|
2018-08-20 22:04:33 +00:00
|
|
|
|
|
|
|
return new Response(
|
|
|
|
200,
|
|
|
|
{},
|
|
|
|
JSON.stringify({ FailedTGAllocs, Diff: generateDiff(req.params.id) })
|
|
|
|
);
|
2018-08-15 23:59:42 +00:00
|
|
|
});
|
|
|
|
|
2018-02-17 02:58:19 +00:00
|
|
|
this.get(
|
|
|
|
'/job/:id',
|
2022-01-26 16:30:06 +00:00
|
|
|
withBlockingSupport(function ({ jobs }, { params, queryParams }) {
|
|
|
|
const job = jobs.all().models.find((job) => {
|
2018-02-17 02:58:19 +00:00
|
|
|
const jobIsDefault = !job.namespaceId || job.namespaceId === 'default';
|
2022-01-26 16:30:06 +00:00
|
|
|
const qpIsDefault =
|
|
|
|
!queryParams.namespace || queryParams.namespace === 'default';
|
2018-02-17 02:58:19 +00:00
|
|
|
return (
|
|
|
|
job.id === params.id &&
|
2022-01-26 16:30:06 +00:00
|
|
|
(job.namespaceId === queryParams.namespace ||
|
|
|
|
(jobIsDefault && qpIsDefault))
|
2018-02-17 02:58:19 +00:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
return job ? this.serialize(job) : new Response(404, {}, null);
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/job/:id', function (schema, req) {
|
2018-08-23 00:36:04 +00:00
|
|
|
const body = JSON.parse(req.requestBody);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
if (!body.Job)
|
|
|
|
return new Response(
|
|
|
|
400,
|
|
|
|
{},
|
|
|
|
'Job is a required field on the request payload'
|
|
|
|
);
|
2018-08-23 00:36:04 +00:00
|
|
|
|
|
|
|
return okEmpty();
|
|
|
|
});
|
|
|
|
|
2018-02-17 02:58:19 +00:00
|
|
|
this.get(
|
|
|
|
'/job/:id/summary',
|
2022-01-26 16:30:06 +00:00
|
|
|
withBlockingSupport(function ({ jobSummaries }, { params }) {
|
2018-02-17 02:58:19 +00:00
|
|
|
return this.serialize(jobSummaries.findBy({ jobId: params.id }));
|
|
|
|
})
|
|
|
|
);
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/job/:id/allocations', function ({ allocations }, { params }) {
|
2017-09-19 14:47:10 +00:00
|
|
|
return this.serialize(allocations.where({ jobId: params.id }));
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/job/:id/versions', function ({ jobVersions }, { params }) {
|
2017-09-19 14:47:10 +00:00
|
|
|
return this.serialize(jobVersions.where({ jobId: params.id }));
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/job/:id/deployments', function ({ deployments }, { params }) {
|
2017-09-19 14:47:10 +00:00
|
|
|
return this.serialize(deployments.where({ jobId: params.id }));
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/job/:id/deployment', function ({ deployments }, { params }) {
|
2018-07-30 22:20:58 +00:00
|
|
|
const deployment = deployments.where({ jobId: params.id }).models[0];
|
2022-01-26 16:30:06 +00:00
|
|
|
return deployment
|
|
|
|
? this.serialize(deployment)
|
|
|
|
: new Response(200, {}, 'null');
|
2018-07-28 00:56:18 +00:00
|
|
|
});
|
|
|
|
|
2020-07-25 04:36:43 +00:00
|
|
|
this.get(
|
|
|
|
'/job/:id/scale',
|
2022-01-26 16:30:06 +00:00
|
|
|
withBlockingSupport(function ({ jobScales }, { params }) {
|
2020-07-25 04:36:43 +00:00
|
|
|
const obj = jobScales.findBy({ jobId: params.id });
|
|
|
|
return this.serialize(jobScales.findBy({ jobId: params.id }));
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/job/:id/periodic/force', function (schema, { params }) {
|
2018-02-02 02:54:23 +00:00
|
|
|
// Create the child job
|
|
|
|
const parent = schema.jobs.find(params.id);
|
|
|
|
|
|
|
|
// Use the server instead of the schema to leverage the job factory
|
|
|
|
server.create('job', 'periodicChild', {
|
|
|
|
parentId: parent.id,
|
|
|
|
namespaceId: parent.namespaceId,
|
|
|
|
namespace: parent.namespace,
|
|
|
|
createAllocations: parent.createAllocations,
|
|
|
|
});
|
|
|
|
|
2018-08-15 23:59:42 +00:00
|
|
|
return okEmpty();
|
2018-02-02 02:54:23 +00:00
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/job/:id/dispatch', function (schema, { params }) {
|
2021-07-20 22:27:41 +00:00
|
|
|
// Create the child job
|
|
|
|
const parent = schema.jobs.find(params.id);
|
|
|
|
|
|
|
|
// Use the server instead of the schema to leverage the job factory
|
|
|
|
let dispatched = server.create('job', 'parameterizedChild', {
|
|
|
|
parentId: parent.id,
|
|
|
|
namespaceId: parent.namespaceId,
|
|
|
|
namespace: parent.namespace,
|
|
|
|
createAllocations: parent.createAllocations,
|
|
|
|
});
|
|
|
|
|
|
|
|
return new Response(
|
|
|
|
200,
|
|
|
|
{},
|
|
|
|
JSON.stringify({
|
|
|
|
DispatchedJobID: dispatched.id,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/job/:id/revert', function ({ jobs }, { requestBody }) {
|
2021-04-20 13:33:16 +00:00
|
|
|
const { JobID, JobVersion } = JSON.parse(requestBody);
|
|
|
|
const job = jobs.find(JobID);
|
|
|
|
job.version = JobVersion;
|
|
|
|
job.save();
|
|
|
|
|
|
|
|
return okEmpty();
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/job/:id/scale', function ({ jobs }, { params }) {
|
2020-06-18 01:07:53 +00:00
|
|
|
return this.serialize(jobs.find(params.id));
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.delete('/job/:id', function (schema, { params }) {
|
2018-04-19 18:13:23 +00:00
|
|
|
const job = schema.jobs.find(params.id);
|
|
|
|
job.update({ status: 'dead' });
|
|
|
|
return new Response(204, {}, '');
|
|
|
|
});
|
|
|
|
|
2017-09-19 14:47:10 +00:00
|
|
|
this.get('/deployment/:id');
|
2021-02-10 14:38:37 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/deployment/fail/:id', function () {
|
2021-02-10 14:38:37 +00:00
|
|
|
return new Response(204, {}, '');
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/deployment/promote/:id', function () {
|
2018-08-24 23:32:08 +00:00
|
|
|
return new Response(204, {}, '');
|
|
|
|
});
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/job/:id/evaluations', function ({ evaluations }, { params }) {
|
2017-11-29 23:36:34 +00:00
|
|
|
return this.serialize(evaluations.where({ jobId: params.id }));
|
|
|
|
});
|
|
|
|
|
2021-12-23 16:54:47 +00:00
|
|
|
this.get('/evaluations');
|
2022-04-27 16:11:24 +00:00
|
|
|
this.get('/evaluation/:id', function ({ evaluations }, { params }) {
|
|
|
|
return evaluations.find(params.id);
|
|
|
|
});
|
2017-11-29 23:36:34 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/deployment/allocations/:id', function (schema, { params }) {
|
2017-09-19 14:47:10 +00:00
|
|
|
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));
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/nodes', function ({ nodes }, req) {
|
2022-01-05 17:49:15 +00:00
|
|
|
// authorize user permissions
|
2022-01-26 16:30:06 +00:00
|
|
|
const token = server.db.tokens.findBy({
|
2022-01-05 17:49:15 +00:00
|
|
|
secretId: req.requestHeaders['X-Nomad-Token'],
|
|
|
|
});
|
2022-01-26 16:30:06 +00:00
|
|
|
|
|
|
|
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.');
|
2022-01-05 17:49:15 +00:00
|
|
|
}
|
2022-01-26 16:30:06 +00:00
|
|
|
|
|
|
|
// TODO: Think about policy handling in Mirage set-up
|
|
|
|
return this.serialize(nodes.all());
|
2017-09-19 14:47:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.get('/node/:id');
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/node/:id/allocations', function ({ allocations }, { params }) {
|
2017-09-19 14:47:10 +00:00
|
|
|
return this.serialize(allocations.where({ nodeId: params.id }));
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post(
|
|
|
|
'/node/:id/eligibility',
|
|
|
|
function ({ nodes }, { params, requestBody }) {
|
|
|
|
const body = JSON.parse(requestBody);
|
|
|
|
const node = nodes.find(params.id);
|
2019-11-02 07:54:37 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
node.update({ schedulingEligibility: body.Elibility === 'eligible' });
|
|
|
|
return this.serialize(node);
|
|
|
|
}
|
|
|
|
);
|
2019-10-25 00:52:21 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/node/:id/drain', function ({ nodes }, { params }) {
|
2019-12-05 00:54:36 +00:00
|
|
|
return this.serialize(nodes.find(params.id));
|
2019-10-25 02:57:46 +00:00
|
|
|
});
|
|
|
|
|
2018-04-21 01:11:32 +00:00
|
|
|
this.get('/allocations');
|
|
|
|
|
2017-09-19 14:47:10 +00:00
|
|
|
this.get('/allocation/:id');
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post('/allocation/:id/stop', function () {
|
2019-05-20 22:27:20 +00:00
|
|
|
return new Response(204, {}, '');
|
|
|
|
});
|
|
|
|
|
2020-03-25 12:51:26 +00:00
|
|
|
this.get(
|
|
|
|
'/volumes',
|
2022-01-26 16:30:06 +00:00
|
|
|
withBlockingSupport(function ({ csiVolumes }, { queryParams }) {
|
2020-03-25 12:51:26 +00:00
|
|
|
if (queryParams.type !== 'csi') {
|
|
|
|
return new Response(200, {}, '[]');
|
|
|
|
}
|
|
|
|
|
2020-04-04 02:24:47 +00:00
|
|
|
const json = this.serialize(csiVolumes.all());
|
|
|
|
const namespace = queryParams.namespace || 'default';
|
2022-01-26 16:30:06 +00:00
|
|
|
return json.filter((volume) => {
|
2021-04-29 20:00:59 +00:00
|
|
|
if (namespace === '*') return true;
|
|
|
|
return namespace === 'default'
|
2020-04-04 02:24:47 +00:00
|
|
|
? !volume.NamespaceID || volume.NamespaceID === namespace
|
2021-04-29 20:00:59 +00:00
|
|
|
: volume.NamespaceID === namespace;
|
|
|
|
});
|
2020-03-25 12:51:26 +00:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
this.get(
|
|
|
|
'/volume/:id',
|
2022-01-26 16:30:06 +00:00
|
|
|
withBlockingSupport(function ({ csiVolumes }, { params, queryParams }) {
|
2020-03-25 12:51:26 +00:00
|
|
|
if (!params.id.startsWith('csi/')) {
|
|
|
|
return new Response(404, {}, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = params.id.replace(/^csi\//, '');
|
2022-01-26 16:30:06 +00:00
|
|
|
const volume = csiVolumes.all().models.find((volume) => {
|
|
|
|
const volumeIsDefault =
|
|
|
|
!volume.namespaceId || volume.namespaceId === 'default';
|
|
|
|
const qpIsDefault =
|
|
|
|
!queryParams.namespace || queryParams.namespace === 'default';
|
2021-04-29 20:00:59 +00:00
|
|
|
return (
|
|
|
|
volume.id === id &&
|
2022-01-26 16:30:06 +00:00
|
|
|
(volume.namespaceId === queryParams.namespace ||
|
|
|
|
(volumeIsDefault && qpIsDefault))
|
2021-04-29 20:00:59 +00:00
|
|
|
);
|
|
|
|
});
|
2020-03-25 12:51:26 +00:00
|
|
|
|
2021-04-29 20:00:59 +00:00
|
|
|
return volume ? this.serialize(volume) : new Response(404, {}, null);
|
2020-03-25 12:51:26 +00:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/plugins', function ({ csiPlugins }, { queryParams }) {
|
2020-03-25 12:51:26 +00:00
|
|
|
if (queryParams.type !== 'csi') {
|
|
|
|
return new Response(200, {}, '[]');
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.serialize(csiPlugins.all());
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/plugin/:id', function ({ csiPlugins }, { params }) {
|
2020-03-25 12:51:26 +00:00
|
|
|
if (!params.id.startsWith('csi/')) {
|
|
|
|
return new Response(404, {}, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = params.id.replace(/^csi\//, '');
|
|
|
|
const volume = csiPlugins.find(id);
|
|
|
|
|
|
|
|
if (!volume) {
|
|
|
|
return new Response(404, {}, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.serialize(volume);
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/namespaces', function ({ namespaces }) {
|
2017-10-10 18:23:10 +00:00
|
|
|
const records = namespaces.all();
|
|
|
|
|
|
|
|
if (records.length) {
|
|
|
|
return this.serialize(records);
|
|
|
|
}
|
|
|
|
|
2021-04-29 20:00:59 +00:00
|
|
|
return this.serialize([{ Name: 'default' }]);
|
2017-10-10 18:23:10 +00:00
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/namespace/:id', function ({ namespaces }, { params }) {
|
2021-04-29 20:00:59 +00:00
|
|
|
return this.serialize(namespaces.find(params.id));
|
2017-10-10 18:23:10 +00:00
|
|
|
});
|
2017-10-07 01:27:36 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/agent/members', function ({ agents, regions }) {
|
2018-08-10 18:20:44 +00:00
|
|
|
const firstRegion = regions.first();
|
2017-09-19 14:47:10 +00:00
|
|
|
return {
|
2018-08-10 18:20:44 +00:00
|
|
|
ServerRegion: firstRegion ? firstRegion.id : null,
|
2022-01-26 16:30:06 +00:00
|
|
|
Members: this.serialize(agents.all()).map(({ member }) => ({
|
|
|
|
...member,
|
|
|
|
})),
|
2017-09-19 14:47:10 +00:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/agent/self', function ({ agents }) {
|
2021-06-16 15:40:24 +00:00
|
|
|
return agents.first();
|
2020-10-26 06:25:28 +00:00
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/agent/monitor', function ({ agents, nodes }, { queryParams }) {
|
2020-06-16 00:59:55 +00:00
|
|
|
const serverId = queryParams.server_id;
|
|
|
|
const clientId = queryParams.client_id;
|
|
|
|
|
|
|
|
if (serverId && clientId)
|
|
|
|
return new Response(400, {}, 'specify a client or a server, not both');
|
2020-06-16 05:47:05 +00:00
|
|
|
if (serverId && !agents.findBy({ name: serverId }))
|
2020-06-16 00:59:55 +00:00
|
|
|
return new Response(400, {}, 'specified server does not exist');
|
|
|
|
if (clientId && !nodes.find(clientId))
|
|
|
|
return new Response(400, {}, 'specified client does not exist');
|
|
|
|
|
|
|
|
if (queryParams.plain) {
|
|
|
|
return logFrames.join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
return logEncode(logFrames, logFrames.length - 1);
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/status/leader', function (schema) {
|
2017-09-19 14:47:10 +00:00
|
|
|
return JSON.stringify(findLeader(schema));
|
|
|
|
});
|
|
|
|
|
2022-12-06 17:45:36 +00:00
|
|
|
this.get('/acl/tokens', function ({tokens}, req) {
|
2022-11-28 15:44:52 +00:00
|
|
|
return this.serialize(tokens.all());
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/acl/token/self', function ({ tokens }, req) {
|
2021-02-02 18:45:40 +00:00
|
|
|
const secret = req.requestHeaders['X-Nomad-Token'];
|
2017-10-14 19:42:14 +00:00
|
|
|
const tokenForSecret = tokens.findBy({ secretId: secret });
|
|
|
|
|
|
|
|
// Return the token if it exists
|
|
|
|
if (tokenForSecret) {
|
|
|
|
return this.serialize(tokenForSecret);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client error if it doesn't
|
|
|
|
return new Response(400, {}, null);
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/acl/token/:id', function ({ tokens }, req) {
|
2017-10-14 18:01:28 +00:00
|
|
|
const token = tokens.find(req.params.id);
|
2021-02-02 18:45:40 +00:00
|
|
|
const secret = req.requestHeaders['X-Nomad-Token'];
|
2017-10-14 18:01:28 +00:00
|
|
|
const tokenForSecret = tokens.findBy({ secretId: secret });
|
|
|
|
|
|
|
|
// Return the token only if the request header matches the token
|
|
|
|
// or the token is of type management
|
2022-01-26 16:30:06 +00:00
|
|
|
if (
|
|
|
|
token.secretId === secret ||
|
|
|
|
(tokenForSecret && tokenForSecret.type === 'management')
|
|
|
|
) {
|
2017-10-14 18:01:28 +00:00
|
|
|
return this.serialize(token);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return not authorized otherwise
|
|
|
|
return new Response(403, {}, null);
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post(
|
|
|
|
'/acl/token/onetime/exchange',
|
|
|
|
function ({ tokens }, { requestBody }) {
|
|
|
|
const { OneTimeSecretID } = JSON.parse(requestBody);
|
2021-04-01 18:21:30 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const tokenForSecret = tokens.findBy({ oneTimeSecret: OneTimeSecretID });
|
2021-04-01 18:21:30 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
// Return the token if it exists
|
|
|
|
if (tokenForSecret) {
|
|
|
|
return {
|
|
|
|
Token: this.serialize(tokenForSecret),
|
|
|
|
};
|
|
|
|
}
|
2021-04-01 18:21:30 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
// Forbidden error if it doesn't
|
|
|
|
return new Response(403, {}, null);
|
|
|
|
}
|
|
|
|
);
|
2021-04-01 18:21:30 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/acl/policy/:id', function ({ policies, tokens }, req) {
|
2022-12-06 17:45:36 +00:00
|
|
|
const policy = policies.findBy({ name: req.params.id });
|
2021-02-02 18:45:40 +00:00
|
|
|
const secret = req.requestHeaders['X-Nomad-Token'];
|
2017-10-14 18:01:28 +00:00
|
|
|
const tokenForSecret = tokens.findBy({ secretId: secret });
|
|
|
|
|
2020-01-20 20:57:01 +00:00
|
|
|
if (req.params.id === 'anonymous') {
|
|
|
|
if (policy) {
|
|
|
|
return this.serialize(policy);
|
|
|
|
} else {
|
|
|
|
return new Response(404, {}, null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-14 18:01:28 +00:00
|
|
|
// Return the policy only if the token that matches the request header
|
|
|
|
// includes the policy or if the token that matches the request header
|
|
|
|
// is of type management
|
|
|
|
if (
|
|
|
|
tokenForSecret &&
|
2022-01-26 16:30:06 +00:00
|
|
|
(tokenForSecret.policies.includes(policy) ||
|
|
|
|
tokenForSecret.type === 'management')
|
2017-10-14 18:01:28 +00:00
|
|
|
) {
|
|
|
|
return this.serialize(policy);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return not authorized otherwise
|
|
|
|
return new Response(403, {}, null);
|
|
|
|
});
|
|
|
|
|
2022-12-06 17:45:36 +00:00
|
|
|
this.get('/acl/policies', function ({ policies }, req) {
|
|
|
|
return this.serialize(policies.all());
|
|
|
|
});
|
|
|
|
|
|
|
|
this.delete('/acl/policy/:id', function (schema, request) {
|
|
|
|
const { id } = request.params;
|
|
|
|
schema.tokens.all().models.filter(token => token.policyIds.includes(id)).forEach(token => {
|
|
|
|
token.update({ policyIds: token.policyIds.filter(pid => pid !== id) });
|
|
|
|
});
|
|
|
|
server.db.policies.remove(id);
|
|
|
|
return '';
|
|
|
|
});
|
|
|
|
|
|
|
|
this.put('/acl/policy/:id', function (schema, request) {
|
|
|
|
return new Response(200, {}, {});
|
|
|
|
});
|
|
|
|
|
|
|
|
this.post('/acl/policy/:id', function (schema, request) {
|
|
|
|
const { Name, Description, Rules } = JSON.parse(request.requestBody);
|
|
|
|
return server.create('policy', {
|
|
|
|
name: Name,
|
|
|
|
description: Description,
|
|
|
|
rules: Rules,
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/regions', function ({ regions }) {
|
2018-08-10 02:57:21 +00:00
|
|
|
return this.serialize(regions.all());
|
2018-08-02 22:54:57 +00:00
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/operator/license', function ({ features }) {
|
2020-11-06 14:21:38 +00:00
|
|
|
const records = features.all();
|
|
|
|
|
|
|
|
if (records.length) {
|
|
|
|
return {
|
|
|
|
License: {
|
|
|
|
Features: records.models.mapBy('name'),
|
2021-04-01 18:21:30 +00:00
|
|
|
},
|
2020-11-06 14:21:38 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Response(501, {}, null);
|
|
|
|
});
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const clientAllocationStatsHandler = function (
|
|
|
|
{ clientAllocationStats },
|
|
|
|
{ params }
|
|
|
|
) {
|
2018-02-23 23:36:38 +00:00
|
|
|
return this.serialize(clientAllocationStats.find(params.id));
|
|
|
|
};
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const clientAllocationLog = function (server, { params, queryParams }) {
|
2018-02-23 23:36:38 +00:00
|
|
|
const allocation = server.allocations.find(params.allocation_id);
|
2022-01-26 16:30:06 +00:00
|
|
|
const tasks = allocation.taskStateIds.map((id) =>
|
|
|
|
server.taskStates.find(id)
|
|
|
|
);
|
2018-02-23 23:36:38 +00:00
|
|
|
|
|
|
|
if (!tasks.mapBy('name').includes(queryParams.task)) {
|
|
|
|
return new Response(400, {}, 'must include task name');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (queryParams.plain) {
|
|
|
|
return logFrames.join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
return logEncode(logFrames, logFrames.length - 1);
|
|
|
|
};
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const clientAllocationFSLsHandler = function (
|
|
|
|
{ allocFiles },
|
|
|
|
{ queryParams: { path } }
|
|
|
|
) {
|
|
|
|
const filterPath = path.endsWith('/')
|
|
|
|
? path.substr(0, path.length - 1)
|
|
|
|
: path;
|
2019-07-31 08:41:00 +00:00
|
|
|
const files = filesForPath(allocFiles, filterPath);
|
2019-07-25 07:47:46 +00:00
|
|
|
return this.serialize(files);
|
2019-07-02 21:42:38 +00:00
|
|
|
};
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const clientAllocationFSStatHandler = function (
|
|
|
|
{ allocFiles },
|
|
|
|
{ queryParams: { path } }
|
|
|
|
) {
|
|
|
|
const filterPath = path.endsWith('/')
|
|
|
|
? path.substr(0, path.length - 1)
|
|
|
|
: path;
|
2019-07-25 07:47:46 +00:00
|
|
|
|
|
|
|
// Root path
|
|
|
|
if (!filterPath) {
|
|
|
|
return this.serialize({
|
|
|
|
IsDir: true,
|
|
|
|
ModTime: new Date(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Either a file or a nested directory
|
|
|
|
const file = allocFiles.where({ path: filterPath }).models[0];
|
|
|
|
return this.serialize(file);
|
2019-07-02 21:42:38 +00:00
|
|
|
};
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const clientAllocationCatHandler = function (
|
|
|
|
{ allocFiles },
|
|
|
|
{ queryParams }
|
|
|
|
) {
|
2019-07-25 08:12:44 +00:00
|
|
|
const [file, err] = fileOrError(allocFiles, queryParams.path);
|
|
|
|
|
|
|
|
if (err) return err;
|
|
|
|
return file.body;
|
|
|
|
};
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const clientAllocationStreamHandler = function (
|
|
|
|
{ allocFiles },
|
|
|
|
{ queryParams }
|
|
|
|
) {
|
2019-07-25 08:12:44 +00:00
|
|
|
const [file, err] = fileOrError(allocFiles, queryParams.path);
|
|
|
|
|
|
|
|
if (err) return err;
|
|
|
|
|
|
|
|
// Pretender, and therefore Mirage, doesn't support streaming responses.
|
|
|
|
return file.body;
|
|
|
|
};
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const clientAllocationReadAtHandler = function (
|
|
|
|
{ allocFiles },
|
|
|
|
{ queryParams }
|
|
|
|
) {
|
2019-07-25 08:12:44 +00:00
|
|
|
const [file, err] = fileOrError(allocFiles, queryParams.path);
|
|
|
|
|
|
|
|
if (err) return err;
|
|
|
|
return file.body.substr(queryParams.offset || 0, queryParams.limit);
|
|
|
|
};
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const fileOrError = function (
|
|
|
|
allocFiles,
|
|
|
|
path,
|
|
|
|
message = 'Operation not allowed on a directory'
|
|
|
|
) {
|
2019-07-25 08:12:44 +00:00
|
|
|
// Root path
|
2020-06-01 13:15:59 +00:00
|
|
|
if (path === '/') {
|
2019-07-25 08:12:44 +00:00
|
|
|
return [null, new Response(400, {}, message)];
|
|
|
|
}
|
|
|
|
|
2020-06-01 13:15:59 +00:00
|
|
|
const file = allocFiles.where({ path }).models[0];
|
2019-07-25 08:12:44 +00:00
|
|
|
if (file.isDir) {
|
|
|
|
return [null, new Response(400, {}, message)];
|
|
|
|
}
|
|
|
|
|
|
|
|
return [file, null];
|
|
|
|
};
|
|
|
|
|
2018-02-23 23:36:38 +00:00
|
|
|
// Client requests are available on the server and the client
|
2022-01-26 16:30:06 +00:00
|
|
|
this.put('/client/allocation/:id/restart', function () {
|
2019-05-20 22:27:20 +00:00
|
|
|
return new Response(204, {}, '');
|
|
|
|
});
|
|
|
|
|
2018-02-23 23:36:38 +00:00
|
|
|
this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
|
|
|
|
this.get('/client/fs/logs/:allocation_id', clientAllocationLog);
|
|
|
|
|
2019-07-02 21:42:38 +00:00
|
|
|
this.get('/client/fs/ls/:allocation_id', clientAllocationFSLsHandler);
|
|
|
|
this.get('/client/fs/stat/:allocation_id', clientAllocationFSStatHandler);
|
2019-07-25 08:12:44 +00:00
|
|
|
this.get('/client/fs/cat/:allocation_id', clientAllocationCatHandler);
|
|
|
|
this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler);
|
|
|
|
this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler);
|
2019-07-02 21:42:38 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get('/client/stats', function ({ clientStats }, { queryParams }) {
|
2019-10-03 14:13:08 +00:00
|
|
|
const seed = faker.random.number(10);
|
|
|
|
if (seed >= 8) {
|
2018-11-02 05:11:56 +00:00
|
|
|
const stats = clientStats.find(queryParams.node_id);
|
|
|
|
stats.update({
|
|
|
|
timestamp: Date.now() * 1000000,
|
2022-01-26 16:30:06 +00:00
|
|
|
CPUTicksConsumed:
|
|
|
|
stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }),
|
2018-11-02 05:11:56 +00:00
|
|
|
});
|
|
|
|
return this.serialize(stats);
|
|
|
|
} else {
|
|
|
|
return new Response(500, {}, null);
|
|
|
|
}
|
2018-02-23 23:36:38 +00:00
|
|
|
});
|
|
|
|
|
2017-09-19 14:47:10 +00:00
|
|
|
// TODO: in the future, this hack may be replaceable with dynamic host name
|
|
|
|
// support in pretender: https://github.com/pretenderjs/pretender/issues/210
|
2022-01-26 16:30:06 +00:00
|
|
|
HOSTS.forEach((host) => {
|
|
|
|
this.get(
|
|
|
|
`http://${host}/v1/client/allocation/:id/stats`,
|
|
|
|
clientAllocationStatsHandler
|
|
|
|
);
|
|
|
|
this.get(
|
|
|
|
`http://${host}/v1/client/fs/logs/:allocation_id`,
|
|
|
|
clientAllocationLog
|
|
|
|
);
|
2017-09-19 14:47:10 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
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
|
|
|
|
);
|
2019-07-02 21:42:38 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get(`http://${host}/v1/client/stats`, function ({ clientStats }) {
|
2017-09-19 14:47:10 +00:00
|
|
|
return this.serialize(clientStats.find(host));
|
|
|
|
});
|
|
|
|
});
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
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);
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
ui: Change global search to use fuzzy search API (#10412)
This updates the UI to use the new fuzzy search API. It’s a drop-in
replacement so the / shortcut to jump to search is preserved, and
results can be cycled through and chosen via arrow keys and the
enter key.
It doesn’t use everything returned by the API:
* deployments and evaluations: these match by id, doesn’t seem like
people would know those or benefit from quick navigation to them
* namespaces: doesn’t seem useful as they currently function
* scaling policies
* tasks: the response doesn’t include an allocation id, which means they
can’t be navigated to in the UI without an additional query
* CSI volumes: aren’t actually returned by the API
Since there’s no API to check the server configuration and know whether
the feature has been disabled, this adds another query in
route:application#beforeModel that acts as feature detection: if the
attempt to query fails (500), the global search field is hidden.
Upon having added another query on load, I realised that beforeModel was
being triggered any time service:router#transitionTo was being called,
which happens upon navigating to a search result, for instance, because
of refreshModel being present on the region query parameter. This PR
adds a check for transition.queryParamsOnly and skips rerunning the
onload queries (token permissions check, license check, fuzzy search
feature detection).
Implementation notes:
* there are changes to unrelated tests to ignore the on-load feature
detection query
* some lifecycle-related guards against undefined were required to
address failures when navigating to an allocation
* the minimum search length of 2 characters is hard-coded as there’s
currently no way to determine min_term_length in the UI
2021-04-28 18:31:05 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.get(
|
|
|
|
'/recommendations',
|
|
|
|
function (
|
|
|
|
{ jobs, namespaces, recommendations },
|
|
|
|
{ queryParams: { job: id, namespace } }
|
|
|
|
) {
|
|
|
|
if (id) {
|
|
|
|
if (!namespaces.all().length) {
|
|
|
|
namespace = null;
|
|
|
|
}
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const job = jobs.findBy({ id, namespace });
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
if (!job) {
|
|
|
|
return [];
|
|
|
|
}
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const taskGroups = job.taskGroups.models;
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const tasks = taskGroups.reduce((tasks, taskGroup) => {
|
|
|
|
return tasks.concat(taskGroup.tasks.models);
|
|
|
|
}, []);
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
const recommendationIds = tasks.reduce((recommendationIds, task) => {
|
|
|
|
return recommendationIds.concat(
|
|
|
|
task.recommendations.models.mapBy('id')
|
|
|
|
);
|
|
|
|
}, []);
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
return recommendations.find(recommendationIds);
|
|
|
|
} else {
|
|
|
|
return recommendations.all();
|
|
|
|
}
|
2020-10-29 12:46:42 +00:00
|
|
|
}
|
2022-01-26 16:30:06 +00:00
|
|
|
);
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
this.post(
|
|
|
|
'/recommendations/apply',
|
|
|
|
function ({ recommendations }, { requestBody }) {
|
|
|
|
const { Apply, Dismiss } = JSON.parse(requestBody);
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
Apply.concat(Dismiss).forEach((id) => {
|
|
|
|
const recommendation = recommendations.find(id);
|
|
|
|
const task = recommendation.task;
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
if (Apply.includes(id)) {
|
|
|
|
task.resources[recommendation.resource] = recommendation.value;
|
|
|
|
}
|
|
|
|
recommendation.destroy();
|
|
|
|
task.save();
|
|
|
|
});
|
2020-10-29 12:46:42 +00:00
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
);
|
2022-05-30 17:10:44 +00:00
|
|
|
|
2022-08-29 18:45:49 +00:00
|
|
|
//#region Variables
|
2022-06-10 14:05:34 +00:00
|
|
|
|
2022-08-09 17:17:55 +00:00
|
|
|
this.get('/vars', function (schema, { queryParams: { namespace } }) {
|
|
|
|
if (namespace && namespace !== '*') {
|
|
|
|
return schema.variables.all().filter((v) => v.namespace === namespace);
|
|
|
|
} else {
|
|
|
|
return schema.variables.all();
|
|
|
|
}
|
2022-05-30 17:10:44 +00:00
|
|
|
});
|
2022-06-10 14:05:34 +00:00
|
|
|
|
2022-05-30 17:10:44 +00:00
|
|
|
this.get('/var/:id', function ({ variables }, { params }) {
|
|
|
|
return variables.find(params.id);
|
|
|
|
});
|
2022-06-10 14:05:34 +00:00
|
|
|
|
2022-05-30 17:10:44 +00:00
|
|
|
this.put('/var/:id', function (schema, request) {
|
|
|
|
const { Path, Namespace, Items } = JSON.parse(request.requestBody);
|
2022-08-15 21:24:34 +00:00
|
|
|
if (request.url.includes('cas=') && Path === 'Auto-conflicting Variable') {
|
|
|
|
return new Response(
|
|
|
|
409,
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
CreateIndex: 65,
|
|
|
|
CreateTime: faker.date.recent(14) * 1000000, // in the past couple weeks
|
|
|
|
Items: { edited_by: 'your_remote_pal' },
|
|
|
|
ModifyIndex: 2118,
|
|
|
|
ModifyTime: faker.date.recent(0.01) * 1000000, // a few minutes ago
|
|
|
|
Namespace: Namespace,
|
|
|
|
Path: Path,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return server.create('variable', {
|
|
|
|
path: Path,
|
|
|
|
namespace: Namespace,
|
|
|
|
items: Items,
|
|
|
|
id: Path,
|
|
|
|
});
|
|
|
|
}
|
2022-05-30 17:10:44 +00:00
|
|
|
});
|
|
|
|
|
2022-06-10 14:05:34 +00:00
|
|
|
this.delete('/var/:id', function (schema, request) {
|
|
|
|
const { id } = request.params;
|
|
|
|
server.db.variables.remove(id);
|
2022-08-15 15:56:09 +00:00
|
|
|
return '';
|
2022-06-10 14:05:34 +00:00
|
|
|
});
|
|
|
|
|
2022-08-29 18:45:49 +00:00
|
|
|
//#endregion Variables
|
2022-08-26 03:22:55 +00:00
|
|
|
|
|
|
|
//#region Services
|
|
|
|
|
2022-09-07 14:23:39 +00:00
|
|
|
const allocationServiceChecksHandler = function (schema) {
|
|
|
|
let disasters = [
|
|
|
|
"Moon's haunted",
|
|
|
|
'reticulating splines',
|
|
|
|
'The operation completed unexpectedly',
|
|
|
|
'Ran out of sriracha :(',
|
|
|
|
'¯\\_(ツ)_/¯',
|
|
|
|
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n "http://www.w3.org/TR/html4/strict.dtd">\n<html>\n <head>\n <meta http-equiv="Content-Type" content="text/html;charset=utf-8">\n <title>Error response</title>\n </head>\n <body>\n <h1>Error response</h1>\n <p>Error code: 404</p>\n <p>Message: File not found.</p>\n <p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>\n </body>\n</html>\n',
|
|
|
|
];
|
|
|
|
let fakeChecks = [];
|
|
|
|
schema.serviceFragments.all().models.forEach((frag, iter) => {
|
|
|
|
[...Array(iter)].forEach((check, checkIter) => {
|
|
|
|
const checkOK = faker.random.boolean();
|
|
|
|
fakeChecks.push({
|
|
|
|
Check: `check-${checkIter}`,
|
|
|
|
Group: `job-name.${frag.taskGroup?.name}[1]`,
|
|
|
|
Output: checkOK
|
|
|
|
? 'nomad: http ok'
|
|
|
|
: disasters[Math.floor(Math.random() * disasters.length)],
|
|
|
|
Service: frag.name,
|
|
|
|
Status: checkOK ? 'success' : 'failure',
|
|
|
|
StatusCode: checkOK ? 200 : 400,
|
|
|
|
Task: frag.task?.name,
|
|
|
|
Timestamp: new Date().getTime(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return fakeChecks;
|
|
|
|
};
|
|
|
|
|
2022-08-26 03:22:55 +00:00
|
|
|
this.get('/job/:id/services', function (schema, { params }) {
|
|
|
|
const { services } = schema;
|
|
|
|
return this.serialize(services.where({ jobId: params.id }));
|
|
|
|
});
|
|
|
|
|
2022-09-07 14:23:39 +00:00
|
|
|
this.get('/client/allocation/:id/checks', allocationServiceChecksHandler);
|
|
|
|
|
2022-08-26 03:22:55 +00:00
|
|
|
//#endregion Services
|
2022-11-28 15:44:52 +00:00
|
|
|
|
|
|
|
//#region SSO
|
|
|
|
this.get('/acl/auth-methods', function (schema, request) {
|
|
|
|
return schema.authMethods.all();
|
|
|
|
});
|
|
|
|
this.post('/acl/oidc/auth-url', (schema, req) => {
|
|
|
|
const {AuthMethod, ClientNonce, RedirectUri, Meta} = JSON.parse(req.requestBody);
|
|
|
|
return new Response(200, {}, {
|
|
|
|
AuthURL: `/ui/oidc-mock?auth_method=${AuthMethod}&client_nonce=${ClientNonce}&redirect_uri=${RedirectUri}&meta=${Meta}`
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// Simulate an OIDC callback by assuming the code passed is the secret of an existing token, and return that token.
|
|
|
|
this.post('/acl/oidc/complete-auth', function (schema, req) {
|
|
|
|
const code = JSON.parse(req.requestBody).Code;
|
|
|
|
const token = schema.tokens.findBy({
|
|
|
|
id: code
|
|
|
|
});
|
|
|
|
|
|
|
|
return new Response(200, {}, {
|
|
|
|
ACLToken: token.secretId
|
|
|
|
});
|
|
|
|
}, {timing: 1000});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#endregion SSO
|
2017-09-19 14:47:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function filterKeys(object, ...keys) {
|
|
|
|
const clone = copy(object, true);
|
|
|
|
|
2022-01-26 16:30:06 +00:00
|
|
|
keys.forEach((key) => {
|
2017-09-19 14:47:10 +00:00
|
|
|
delete clone[key];
|
|
|
|
});
|
|
|
|
|
|
|
|
return clone;
|
|
|
|
}
|
2018-08-15 23:59:42 +00:00
|
|
|
|
|
|
|
// An empty response but not a 204 No Content. This is still a valid JSON
|
|
|
|
// response that represents a payload with no worthwhile data.
|
|
|
|
function okEmpty() {
|
|
|
|
return new Response(200, {}, '{}');
|
|
|
|
}
|
2018-08-20 22:04:33 +00:00
|
|
|
|
|
|
|
function generateFailedTGAllocs(job, taskGroups) {
|
|
|
|
const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name');
|
|
|
|
|
|
|
|
let tgNames = ['tg-one', 'tg-two'];
|
2022-01-26 16:30:06 +00:00
|
|
|
if (taskGroupsFromSpec && taskGroupsFromSpec.length)
|
|
|
|
tgNames = taskGroupsFromSpec;
|
2018-08-20 22:04:33 +00:00
|
|
|
if (taskGroups && taskGroups.length) tgNames = taskGroups;
|
|
|
|
|
|
|
|
return tgNames.reduce((hash, tgName) => {
|
|
|
|
hash[tgName] = generateTaskGroupFailures();
|
|
|
|
return hash;
|
|
|
|
}, {});
|
|
|
|
}
|