github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/ui/mirage/config.js (about) 1 import Ember from 'ember'; 2 import Response from 'ember-cli-mirage/response'; 3 import { HOSTS } from './common'; 4 import { logFrames, logEncode } from './data/logs'; 5 import { generateDiff } from './factories/job-version'; 6 import { generateTaskGroupFailures } from './factories/evaluation'; 7 import { copy } from 'ember-copy'; 8 9 export function findLeader(schema) { 10 const agent = schema.agents.first(); 11 return `${agent.address}:${agent.tags.port}`; 12 } 13 14 export function filesForPath(allocFiles, filterPath) { 15 return allocFiles.where( 16 file => 17 (!filterPath || file.path.startsWith(filterPath)) && 18 file.path.length > filterPath.length && 19 !file.path.substr(filterPath.length + 1).includes('/') 20 ); 21 } 22 23 export default function() { 24 this.timing = 0; // delay for each request, automatically set to 0 during testing 25 26 this.logging = window.location.search.includes('mirage-logging=true'); 27 28 this.namespace = 'v1'; 29 this.trackRequests = Ember.testing; 30 31 const nomadIndices = {}; // used for tracking blocking queries 32 const server = this; 33 const withBlockingSupport = function(fn) { 34 return function(schema, request) { 35 // Get the original response 36 let { url } = request; 37 url = url.replace(/index=\d+[&;]?/, ''); 38 const response = fn.apply(this, arguments); 39 40 // Get and increment the appropriate index 41 nomadIndices[url] || (nomadIndices[url] = 2); 42 const index = nomadIndices[url]; 43 nomadIndices[url]++; 44 45 // Annotate the response with the index 46 if (response instanceof Response) { 47 response.headers['X-Nomad-Index'] = index; 48 return response; 49 } 50 return new Response(200, { 'x-nomad-index': index }, response); 51 }; 52 }; 53 54 this.get( 55 '/jobs', 56 withBlockingSupport(function({ jobs }, { queryParams }) { 57 const json = this.serialize(jobs.all()); 58 const namespace = queryParams.namespace || 'default'; 59 return json 60 .filter(job => 61 namespace === 'default' 62 ? !job.NamespaceID || job.NamespaceID === namespace 63 : job.NamespaceID === namespace 64 ) 65 .map(job => filterKeys(job, 'TaskGroups', 'NamespaceID')); 66 }) 67 ); 68 69 this.post('/jobs', function(schema, req) { 70 const body = JSON.parse(req.requestBody); 71 72 if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); 73 74 return okEmpty(); 75 }); 76 77 this.post('/jobs/parse', function(schema, req) { 78 const body = JSON.parse(req.requestBody); 79 80 if (!body.JobHCL) 81 return new Response(400, {}, 'JobHCL is a required field on the request payload'); 82 if (!body.Canonicalize) return new Response(400, {}, 'Expected Canonicalize to be true'); 83 84 // Parse the name out of the first real line of HCL to match IDs in the new job record 85 // Regex expectation: 86 // in: job "job-name" { 87 // out: job-name 88 const nameFromHCLBlock = /.+?"(.+?)"/; 89 const jobName = body.JobHCL.trim() 90 .split('\n')[0] 91 .match(nameFromHCLBlock)[1]; 92 93 const job = server.create('job', { id: jobName }); 94 return new Response(200, {}, this.serialize(job)); 95 }); 96 97 this.post('/job/:id/plan', function(schema, req) { 98 const body = JSON.parse(req.requestBody); 99 100 if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); 101 if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true'); 102 103 const FailedTGAllocs = body.Job.Unschedulable && generateFailedTGAllocs(body.Job); 104 105 return new Response( 106 200, 107 {}, 108 JSON.stringify({ FailedTGAllocs, Diff: generateDiff(req.params.id) }) 109 ); 110 }); 111 112 this.get( 113 '/job/:id', 114 withBlockingSupport(function({ jobs }, { params, queryParams }) { 115 const job = jobs.all().models.find(job => { 116 const jobIsDefault = !job.namespaceId || job.namespaceId === 'default'; 117 const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default'; 118 return ( 119 job.id === params.id && 120 (job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault)) 121 ); 122 }); 123 124 return job ? this.serialize(job) : new Response(404, {}, null); 125 }) 126 ); 127 128 this.post('/job/:id', function(schema, req) { 129 const body = JSON.parse(req.requestBody); 130 131 if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); 132 133 return okEmpty(); 134 }); 135 136 this.get( 137 '/job/:id/summary', 138 withBlockingSupport(function({ jobSummaries }, { params }) { 139 return this.serialize(jobSummaries.findBy({ jobId: params.id })); 140 }) 141 ); 142 143 this.get('/job/:id/allocations', function({ allocations }, { params }) { 144 return this.serialize(allocations.where({ jobId: params.id })); 145 }); 146 147 this.get('/job/:id/versions', function({ jobVersions }, { params }) { 148 return this.serialize(jobVersions.where({ jobId: params.id })); 149 }); 150 151 this.get('/job/:id/deployments', function({ deployments }, { params }) { 152 return this.serialize(deployments.where({ jobId: params.id })); 153 }); 154 155 this.get('/job/:id/deployment', function({ deployments }, { params }) { 156 const deployment = deployments.where({ jobId: params.id }).models[0]; 157 return deployment ? this.serialize(deployment) : new Response(200, {}, 'null'); 158 }); 159 160 this.post('/job/:id/periodic/force', function(schema, { params }) { 161 // Create the child job 162 const parent = schema.jobs.find(params.id); 163 164 // Use the server instead of the schema to leverage the job factory 165 server.create('job', 'periodicChild', { 166 parentId: parent.id, 167 namespaceId: parent.namespaceId, 168 namespace: parent.namespace, 169 createAllocations: parent.createAllocations, 170 }); 171 172 return okEmpty(); 173 }); 174 175 this.delete('/job/:id', function(schema, { params }) { 176 const job = schema.jobs.find(params.id); 177 job.update({ status: 'dead' }); 178 return new Response(204, {}, ''); 179 }); 180 181 this.get('/deployment/:id'); 182 this.post('/deployment/promote/:id', function() { 183 return new Response(204, {}, ''); 184 }); 185 186 this.get('/job/:id/evaluations', function({ evaluations }, { params }) { 187 return this.serialize(evaluations.where({ jobId: params.id })); 188 }); 189 190 this.get('/evaluation/:id'); 191 192 this.get('/deployment/allocations/:id', function(schema, { params }) { 193 const job = schema.jobs.find(schema.deployments.find(params.id).jobId); 194 const allocations = schema.allocations.where({ jobId: job.id }); 195 196 return this.serialize(allocations.slice(0, 3)); 197 }); 198 199 this.get('/nodes', function({ nodes }) { 200 const json = this.serialize(nodes.all()); 201 return json; 202 }); 203 204 this.get('/node/:id'); 205 206 this.get('/node/:id/allocations', function({ allocations }, { params }) { 207 return this.serialize(allocations.where({ nodeId: params.id })); 208 }); 209 210 this.post('/node/:id/eligibility', function({ nodes }, { params, requestBody }) { 211 const body = JSON.parse(requestBody); 212 const node = nodes.find(params.id); 213 214 node.update({ schedulingEligibility: body.Elibility === 'eligible' }); 215 return this.serialize(node); 216 }); 217 218 this.post('/node/:id/drain', function({ nodes }, { params }) { 219 return this.serialize(nodes.find(params.id)); 220 }); 221 222 this.get('/allocations'); 223 224 this.get('/allocation/:id'); 225 226 this.post('/allocation/:id/stop', function() { 227 return new Response(204, {}, ''); 228 }); 229 230 this.get( 231 '/volumes', 232 withBlockingSupport(function({ csiVolumes }, { queryParams }) { 233 if (queryParams.type !== 'csi') { 234 return new Response(200, {}, '[]'); 235 } 236 237 const json = this.serialize(csiVolumes.all()); 238 const namespace = queryParams.namespace || 'default'; 239 return json.filter(volume => 240 namespace === 'default' 241 ? !volume.NamespaceID || volume.NamespaceID === namespace 242 : volume.NamespaceID === namespace 243 ); 244 }) 245 ); 246 247 this.get( 248 '/volume/:id', 249 withBlockingSupport(function({ csiVolumes }, { params }) { 250 if (!params.id.startsWith('csi/')) { 251 return new Response(404, {}, null); 252 } 253 254 const id = params.id.replace(/^csi\//, ''); 255 const volume = csiVolumes.find(id); 256 257 if (!volume) { 258 return new Response(404, {}, null); 259 } 260 261 return this.serialize(volume); 262 }) 263 ); 264 265 this.get('/plugins', function({ csiPlugins }, { queryParams }) { 266 if (queryParams.type !== 'csi') { 267 return new Response(200, {}, '[]'); 268 } 269 270 return this.serialize(csiPlugins.all()); 271 }); 272 273 this.get('/plugin/:id', function({ csiPlugins }, { params }) { 274 if (!params.id.startsWith('csi/')) { 275 return new Response(404, {}, null); 276 } 277 278 const id = params.id.replace(/^csi\//, ''); 279 const volume = csiPlugins.find(id); 280 281 if (!volume) { 282 return new Response(404, {}, null); 283 } 284 285 return this.serialize(volume); 286 }); 287 288 this.get('/namespaces', function({ namespaces }) { 289 const records = namespaces.all(); 290 291 if (records.length) { 292 return this.serialize(records); 293 } 294 295 return new Response(501, {}, null); 296 }); 297 298 this.get('/namespace/:id', function({ namespaces }, { params }) { 299 if (namespaces.all().length) { 300 return this.serialize(namespaces.find(params.id)); 301 } 302 303 return new Response(501, {}, null); 304 }); 305 306 this.get('/agent/members', function({ agents, regions }) { 307 const firstRegion = regions.first(); 308 return { 309 ServerRegion: firstRegion ? firstRegion.id : null, 310 Members: this.serialize(agents.all()), 311 }; 312 }); 313 314 this.get('/status/leader', function(schema) { 315 return JSON.stringify(findLeader(schema)); 316 }); 317 318 this.get('/acl/token/self', function({ tokens }, req) { 319 const secret = req.requestHeaders['X-Nomad-Token']; 320 const tokenForSecret = tokens.findBy({ secretId: secret }); 321 322 // Return the token if it exists 323 if (tokenForSecret) { 324 return this.serialize(tokenForSecret); 325 } 326 327 // Client error if it doesn't 328 return new Response(400, {}, null); 329 }); 330 331 this.get('/acl/token/:id', function({ tokens }, req) { 332 const token = tokens.find(req.params.id); 333 const secret = req.requestHeaders['X-Nomad-Token']; 334 const tokenForSecret = tokens.findBy({ secretId: secret }); 335 336 // Return the token only if the request header matches the token 337 // or the token is of type management 338 if (token.secretId === secret || (tokenForSecret && tokenForSecret.type === 'management')) { 339 return this.serialize(token); 340 } 341 342 // Return not authorized otherwise 343 return new Response(403, {}, null); 344 }); 345 346 this.get('/acl/policy/:id', function({ policies, tokens }, req) { 347 const policy = policies.find(req.params.id); 348 const secret = req.requestHeaders['X-Nomad-Token']; 349 const tokenForSecret = tokens.findBy({ secretId: secret }); 350 351 if (req.params.id === 'anonymous') { 352 if (policy) { 353 return this.serialize(policy); 354 } else { 355 return new Response(404, {}, null); 356 } 357 } 358 359 // Return the policy only if the token that matches the request header 360 // includes the policy or if the token that matches the request header 361 // is of type management 362 if ( 363 tokenForSecret && 364 (tokenForSecret.policies.includes(policy) || tokenForSecret.type === 'management') 365 ) { 366 return this.serialize(policy); 367 } 368 369 // Return not authorized otherwise 370 return new Response(403, {}, null); 371 }); 372 373 this.get('/regions', function({ regions }) { 374 return this.serialize(regions.all()); 375 }); 376 377 const clientAllocationStatsHandler = function({ clientAllocationStats }, { params }) { 378 return this.serialize(clientAllocationStats.find(params.id)); 379 }; 380 381 const clientAllocationLog = function(server, { params, queryParams }) { 382 const allocation = server.allocations.find(params.allocation_id); 383 const tasks = allocation.taskStateIds.map(id => server.taskStates.find(id)); 384 385 if (!tasks.mapBy('name').includes(queryParams.task)) { 386 return new Response(400, {}, 'must include task name'); 387 } 388 389 if (queryParams.plain) { 390 return logFrames.join(''); 391 } 392 393 return logEncode(logFrames, logFrames.length - 1); 394 }; 395 396 const clientAllocationFSLsHandler = function({ allocFiles }, { queryParams }) { 397 // Ignore the task name at the beginning of the path 398 const filterPath = queryParams.path.substr(queryParams.path.indexOf('/') + 1); 399 const files = filesForPath(allocFiles, filterPath); 400 return this.serialize(files); 401 }; 402 403 const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams }) { 404 // Ignore the task name at the beginning of the path 405 const filterPath = queryParams.path.substr(queryParams.path.indexOf('/') + 1); 406 407 // Root path 408 if (!filterPath) { 409 return this.serialize({ 410 IsDir: true, 411 ModTime: new Date(), 412 }); 413 } 414 415 // Either a file or a nested directory 416 const file = allocFiles.where({ path: filterPath }).models[0]; 417 return this.serialize(file); 418 }; 419 420 const clientAllocationCatHandler = function({ allocFiles }, { queryParams }) { 421 const [file, err] = fileOrError(allocFiles, queryParams.path); 422 423 if (err) return err; 424 return file.body; 425 }; 426 427 const clientAllocationStreamHandler = function({ allocFiles }, { queryParams }) { 428 const [file, err] = fileOrError(allocFiles, queryParams.path); 429 430 if (err) return err; 431 432 // Pretender, and therefore Mirage, doesn't support streaming responses. 433 return file.body; 434 }; 435 436 const clientAllocationReadAtHandler = function({ allocFiles }, { queryParams }) { 437 const [file, err] = fileOrError(allocFiles, queryParams.path); 438 439 if (err) return err; 440 return file.body.substr(queryParams.offset || 0, queryParams.limit); 441 }; 442 443 const fileOrError = function(allocFiles, path, message = 'Operation not allowed on a directory') { 444 // Ignore the task name at the beginning of the path 445 const filterPath = path.substr(path.indexOf('/') + 1); 446 447 // Root path 448 if (!filterPath) { 449 return [null, new Response(400, {}, message)]; 450 } 451 452 const file = allocFiles.where({ path: filterPath }).models[0]; 453 if (file.isDir) { 454 return [null, new Response(400, {}, message)]; 455 } 456 457 return [file, null]; 458 }; 459 460 // Client requests are available on the server and the client 461 this.put('/client/allocation/:id/restart', function() { 462 return new Response(204, {}, ''); 463 }); 464 465 this.get('/client/allocation/:id/stats', clientAllocationStatsHandler); 466 this.get('/client/fs/logs/:allocation_id', clientAllocationLog); 467 468 this.get('/client/fs/ls/:allocation_id', clientAllocationFSLsHandler); 469 this.get('/client/fs/stat/:allocation_id', clientAllocationFSStatHandler); 470 this.get('/client/fs/cat/:allocation_id', clientAllocationCatHandler); 471 this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler); 472 this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler); 473 474 this.get('/client/stats', function({ clientStats }, { queryParams }) { 475 const seed = faker.random.number(10); 476 if (seed >= 8) { 477 const stats = clientStats.find(queryParams.node_id); 478 stats.update({ 479 timestamp: Date.now() * 1000000, 480 CPUTicksConsumed: stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }), 481 }); 482 return this.serialize(stats); 483 } else { 484 return new Response(500, {}, null); 485 } 486 }); 487 488 // TODO: in the future, this hack may be replaceable with dynamic host name 489 // support in pretender: https://github.com/pretenderjs/pretender/issues/210 490 HOSTS.forEach(host => { 491 this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler); 492 this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog); 493 494 this.get(`http://${host}/v1/client/fs/ls/:allocation_id`, clientAllocationFSLsHandler); 495 this.get(`http://${host}/v1/client/stat/ls/:allocation_id`, clientAllocationFSStatHandler); 496 this.get(`http://${host}/v1/client/fs/cat/:allocation_id`, clientAllocationCatHandler); 497 this.get(`http://${host}/v1/client/fs/stream/:allocation_id`, clientAllocationStreamHandler); 498 this.get(`http://${host}/v1/client/fs/readat/:allocation_id`, clientAllocationReadAtHandler); 499 500 this.get(`http://${host}/v1/client/stats`, function({ clientStats }) { 501 return this.serialize(clientStats.find(host)); 502 }); 503 }); 504 } 505 506 function filterKeys(object, ...keys) { 507 const clone = copy(object, true); 508 509 keys.forEach(key => { 510 delete clone[key]; 511 }); 512 513 return clone; 514 } 515 516 // An empty response but not a 204 No Content. This is still a valid JSON 517 // response that represents a payload with no worthwhile data. 518 function okEmpty() { 519 return new Response(200, {}, '{}'); 520 } 521 522 function generateFailedTGAllocs(job, taskGroups) { 523 const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name'); 524 525 let tgNames = ['tg-one', 'tg-two']; 526 if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec; 527 if (taskGroups && taskGroups.length) tgNames = taskGroups; 528 529 return tgNames.reduce((hash, tgName) => { 530 hash[tgName] = generateTaskGroupFailures(); 531 return hash; 532 }, {}); 533 }