github.com/aminovpavel/nomad@v0.11.8/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: { path } }) { 397 const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; 398 const files = filesForPath(allocFiles, filterPath); 399 return this.serialize(files); 400 }; 401 402 const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams: { path } }) { 403 const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path; 404 405 // Root path 406 if (!filterPath) { 407 return this.serialize({ 408 IsDir: true, 409 ModTime: new Date(), 410 }); 411 } 412 413 // Either a file or a nested directory 414 const file = allocFiles.where({ path: filterPath }).models[0]; 415 return this.serialize(file); 416 }; 417 418 const clientAllocationCatHandler = function({ allocFiles }, { queryParams }) { 419 const [file, err] = fileOrError(allocFiles, queryParams.path); 420 421 if (err) return err; 422 return file.body; 423 }; 424 425 const clientAllocationStreamHandler = function({ allocFiles }, { queryParams }) { 426 const [file, err] = fileOrError(allocFiles, queryParams.path); 427 428 if (err) return err; 429 430 // Pretender, and therefore Mirage, doesn't support streaming responses. 431 return file.body; 432 }; 433 434 const clientAllocationReadAtHandler = function({ allocFiles }, { queryParams }) { 435 const [file, err] = fileOrError(allocFiles, queryParams.path); 436 437 if (err) return err; 438 return file.body.substr(queryParams.offset || 0, queryParams.limit); 439 }; 440 441 const fileOrError = function(allocFiles, path, message = 'Operation not allowed on a directory') { 442 // Root path 443 if (path === '/') { 444 return [null, new Response(400, {}, message)]; 445 } 446 447 const file = allocFiles.where({ path }).models[0]; 448 if (file.isDir) { 449 return [null, new Response(400, {}, message)]; 450 } 451 452 return [file, null]; 453 }; 454 455 // Client requests are available on the server and the client 456 this.put('/client/allocation/:id/restart', function() { 457 return new Response(204, {}, ''); 458 }); 459 460 this.get('/client/allocation/:id/stats', clientAllocationStatsHandler); 461 this.get('/client/fs/logs/:allocation_id', clientAllocationLog); 462 463 this.get('/client/fs/ls/:allocation_id', clientAllocationFSLsHandler); 464 this.get('/client/fs/stat/:allocation_id', clientAllocationFSStatHandler); 465 this.get('/client/fs/cat/:allocation_id', clientAllocationCatHandler); 466 this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler); 467 this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler); 468 469 this.get('/client/stats', function({ clientStats }, { queryParams }) { 470 const seed = faker.random.number(10); 471 if (seed >= 8) { 472 const stats = clientStats.find(queryParams.node_id); 473 stats.update({ 474 timestamp: Date.now() * 1000000, 475 CPUTicksConsumed: stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }), 476 }); 477 return this.serialize(stats); 478 } else { 479 return new Response(500, {}, null); 480 } 481 }); 482 483 // TODO: in the future, this hack may be replaceable with dynamic host name 484 // support in pretender: https://github.com/pretenderjs/pretender/issues/210 485 HOSTS.forEach(host => { 486 this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler); 487 this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog); 488 489 this.get(`http://${host}/v1/client/fs/ls/:allocation_id`, clientAllocationFSLsHandler); 490 this.get(`http://${host}/v1/client/stat/ls/:allocation_id`, clientAllocationFSStatHandler); 491 this.get(`http://${host}/v1/client/fs/cat/:allocation_id`, clientAllocationCatHandler); 492 this.get(`http://${host}/v1/client/fs/stream/:allocation_id`, clientAllocationStreamHandler); 493 this.get(`http://${host}/v1/client/fs/readat/:allocation_id`, clientAllocationReadAtHandler); 494 495 this.get(`http://${host}/v1/client/stats`, function({ clientStats }) { 496 return this.serialize(clientStats.find(host)); 497 }); 498 }); 499 } 500 501 function filterKeys(object, ...keys) { 502 const clone = copy(object, true); 503 504 keys.forEach(key => { 505 delete clone[key]; 506 }); 507 508 return clone; 509 } 510 511 // An empty response but not a 204 No Content. This is still a valid JSON 512 // response that represents a payload with no worthwhile data. 513 function okEmpty() { 514 return new Response(200, {}, '{}'); 515 } 516 517 function generateFailedTGAllocs(job, taskGroups) { 518 const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name'); 519 520 let tgNames = ['tg-one', 'tg-two']; 521 if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec; 522 if (taskGroups && taskGroups.length) tgNames = taskGroups; 523 524 return tgNames.reduce((hash, tgName) => { 525 hash[tgName] = generateTaskGroupFailures(); 526 return hash; 527 }, {}); 528 }