github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/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 import formatHost from 'nomad-ui/utils/format-host'; 9 import faker from 'nomad-ui/mirage/faker'; 10 11 export function findLeader(schema) { 12 const agent = schema.agents.first(); 13 return formatHost(agent.member.Address, agent.member.Tags.port); 14 } 15 16 export function filesForPath(allocFiles, filterPath) { 17 return allocFiles.where( 18 (file) => 19 (!filterPath || file.path.startsWith(filterPath)) && 20 file.path.length > filterPath.length && 21 !file.path.substr(filterPath.length + 1).includes('/') 22 ); 23 } 24 25 export default function () { 26 this.timing = 0; // delay for each request, automatically set to 0 during testing 27 28 this.logging = window.location.search.includes('mirage-logging=true'); 29 30 this.namespace = 'v1'; 31 this.trackRequests = Ember.testing; 32 33 const nomadIndices = {}; // used for tracking blocking queries 34 const server = this; 35 const withBlockingSupport = function (fn) { 36 return function (schema, request) { 37 // Get the original response 38 let { url } = request; 39 url = url.replace(/index=\d+[&;]?/, ''); 40 const response = fn.apply(this, arguments); 41 42 // Get and increment the appropriate index 43 nomadIndices[url] || (nomadIndices[url] = 2); 44 const index = nomadIndices[url]; 45 nomadIndices[url]++; 46 47 // Annotate the response with the index 48 if (response instanceof Response) { 49 response.headers['x-nomad-index'] = index; 50 return response; 51 } 52 return new Response(200, { 'x-nomad-index': index }, response); 53 }; 54 }; 55 56 this.get( 57 '/jobs', 58 withBlockingSupport(function ({ jobs }, { queryParams }) { 59 const json = this.serialize(jobs.all()); 60 const namespace = queryParams.namespace || 'default'; 61 return json 62 .filter((job) => { 63 if (namespace === '*') return true; 64 return namespace === 'default' 65 ? !job.NamespaceID || job.NamespaceID === namespace 66 : job.NamespaceID === namespace; 67 }) 68 .map((job) => filterKeys(job, 'TaskGroups', 'NamespaceID')); 69 }) 70 ); 71 72 this.post('/jobs', function (schema, req) { 73 const body = JSON.parse(req.requestBody); 74 75 if (!body.Job) 76 return new Response( 77 400, 78 {}, 79 'Job is a required field on the request payload' 80 ); 81 82 return okEmpty(); 83 }); 84 85 this.post('/jobs/parse', function (schema, req) { 86 const body = JSON.parse(req.requestBody); 87 88 if (!body.JobHCL) 89 return new Response( 90 400, 91 {}, 92 'JobHCL is a required field on the request payload' 93 ); 94 if (!body.Canonicalize) 95 return new Response(400, {}, 'Expected Canonicalize to be true'); 96 97 // Parse the name out of the first real line of HCL to match IDs in the new job record 98 // Regex expectation: 99 // in: job "job-name" { 100 // out: job-name 101 const nameFromHCLBlock = /.+?"(.+?)"/; 102 const jobName = body.JobHCL.trim() 103 .split('\n')[0] 104 .match(nameFromHCLBlock)[1]; 105 106 const job = server.create('job', { id: jobName }); 107 return new Response(200, {}, this.serialize(job)); 108 }); 109 110 this.post('/job/:id/plan', function (schema, req) { 111 const body = JSON.parse(req.requestBody); 112 113 if (!body.Job) 114 return new Response( 115 400, 116 {}, 117 'Job is a required field on the request payload' 118 ); 119 if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true'); 120 121 const FailedTGAllocs = 122 body.Job.Unschedulable && generateFailedTGAllocs(body.Job); 123 124 return new Response( 125 200, 126 {}, 127 JSON.stringify({ FailedTGAllocs, Diff: generateDiff(req.params.id) }) 128 ); 129 }); 130 131 this.get( 132 '/job/:id', 133 withBlockingSupport(function ({ jobs }, { params, queryParams }) { 134 const job = jobs.all().models.find((job) => { 135 const jobIsDefault = !job.namespaceId || job.namespaceId === 'default'; 136 const qpIsDefault = 137 !queryParams.namespace || queryParams.namespace === 'default'; 138 return ( 139 job.id === params.id && 140 (job.namespaceId === queryParams.namespace || 141 (jobIsDefault && qpIsDefault)) 142 ); 143 }); 144 145 return job ? this.serialize(job) : new Response(404, {}, null); 146 }) 147 ); 148 149 this.post('/job/:id', function (schema, req) { 150 const body = JSON.parse(req.requestBody); 151 152 if (!body.Job) 153 return new Response( 154 400, 155 {}, 156 'Job is a required field on the request payload' 157 ); 158 159 return okEmpty(); 160 }); 161 162 this.get( 163 '/job/:id/summary', 164 withBlockingSupport(function ({ jobSummaries }, { params }) { 165 return this.serialize(jobSummaries.findBy({ jobId: params.id })); 166 }) 167 ); 168 169 this.get('/job/:id/allocations', function ({ allocations }, { params }) { 170 return this.serialize(allocations.where({ jobId: params.id })); 171 }); 172 173 this.get('/job/:id/versions', function ({ jobVersions }, { params }) { 174 return this.serialize(jobVersions.where({ jobId: params.id })); 175 }); 176 177 this.get('/job/:id/deployments', function ({ deployments }, { params }) { 178 return this.serialize(deployments.where({ jobId: params.id })); 179 }); 180 181 this.get('/job/:id/deployment', function ({ deployments }, { params }) { 182 const deployment = deployments.where({ jobId: params.id }).models[0]; 183 return deployment 184 ? this.serialize(deployment) 185 : new Response(200, {}, 'null'); 186 }); 187 188 this.get( 189 '/job/:id/scale', 190 withBlockingSupport(function ({ jobScales }, { params }) { 191 const obj = jobScales.findBy({ jobId: params.id }); 192 return this.serialize(jobScales.findBy({ jobId: params.id })); 193 }) 194 ); 195 196 this.post('/job/:id/periodic/force', function (schema, { params }) { 197 // Create the child job 198 const parent = schema.jobs.find(params.id); 199 200 // Use the server instead of the schema to leverage the job factory 201 server.create('job', 'periodicChild', { 202 parentId: parent.id, 203 namespaceId: parent.namespaceId, 204 namespace: parent.namespace, 205 createAllocations: parent.createAllocations, 206 }); 207 208 return okEmpty(); 209 }); 210 211 this.post('/job/:id/dispatch', function (schema, { params }) { 212 // Create the child job 213 const parent = schema.jobs.find(params.id); 214 215 // Use the server instead of the schema to leverage the job factory 216 let dispatched = server.create('job', 'parameterizedChild', { 217 parentId: parent.id, 218 namespaceId: parent.namespaceId, 219 namespace: parent.namespace, 220 createAllocations: parent.createAllocations, 221 }); 222 223 return new Response( 224 200, 225 {}, 226 JSON.stringify({ 227 DispatchedJobID: dispatched.id, 228 }) 229 ); 230 }); 231 232 this.post('/job/:id/revert', function ({ jobs }, { requestBody }) { 233 const { JobID, JobVersion } = JSON.parse(requestBody); 234 const job = jobs.find(JobID); 235 job.version = JobVersion; 236 job.save(); 237 238 return okEmpty(); 239 }); 240 241 this.post('/job/:id/scale', function ({ jobs }, { params }) { 242 return this.serialize(jobs.find(params.id)); 243 }); 244 245 this.delete('/job/:id', function (schema, { params }) { 246 const job = schema.jobs.find(params.id); 247 job.update({ status: 'dead' }); 248 return new Response(204, {}, ''); 249 }); 250 251 this.get('/deployment/:id'); 252 253 this.post('/deployment/fail/:id', function () { 254 return new Response(204, {}, ''); 255 }); 256 257 this.post('/deployment/promote/:id', function () { 258 return new Response(204, {}, ''); 259 }); 260 261 this.get('/job/:id/evaluations', function ({ evaluations }, { params }) { 262 return this.serialize(evaluations.where({ jobId: params.id })); 263 }); 264 265 this.get('/evaluations'); 266 this.get('/evaluation/:id', function ({ evaluations }, { params }) { 267 return evaluations.find(params.id); 268 }); 269 270 this.get('/deployment/allocations/:id', function (schema, { params }) { 271 const job = schema.jobs.find(schema.deployments.find(params.id).jobId); 272 const allocations = schema.allocations.where({ jobId: job.id }); 273 274 return this.serialize(allocations.slice(0, 3)); 275 }); 276 277 this.get('/nodes', function ({ nodes }, req) { 278 // authorize user permissions 279 const token = server.db.tokens.findBy({ 280 secretId: req.requestHeaders['X-Nomad-Token'], 281 }); 282 283 if (token) { 284 const { policyIds } = token; 285 const policies = server.db.policies.find(policyIds); 286 const hasReadPolicy = policies.find( 287 (p) => 288 p.rulesJSON.Node?.Policy === 'read' || 289 p.rulesJSON.Node?.Policy === 'write' 290 ); 291 if (hasReadPolicy) { 292 const json = this.serialize(nodes.all()); 293 return json; 294 } 295 return new Response(403, {}, 'Permissions have not be set-up.'); 296 } 297 298 // TODO: Think about policy handling in Mirage set-up 299 return this.serialize(nodes.all()); 300 }); 301 302 this.get('/node/:id'); 303 304 this.get('/node/:id/allocations', function ({ allocations }, { params }) { 305 return this.serialize(allocations.where({ nodeId: params.id })); 306 }); 307 308 this.post( 309 '/node/:id/eligibility', 310 function ({ nodes }, { params, requestBody }) { 311 const body = JSON.parse(requestBody); 312 const node = nodes.find(params.id); 313 314 node.update({ schedulingEligibility: body.Elibility === 'eligible' }); 315 return this.serialize(node); 316 } 317 ); 318 319 this.post('/node/:id/drain', function ({ nodes }, { params }) { 320 return this.serialize(nodes.find(params.id)); 321 }); 322 323 this.get('/allocations'); 324 325 this.get('/allocation/:id'); 326 327 this.post('/allocation/:id/stop', function () { 328 return new Response(204, {}, ''); 329 }); 330 331 this.get( 332 '/volumes', 333 withBlockingSupport(function ({ csiVolumes }, { queryParams }) { 334 if (queryParams.type !== 'csi') { 335 return new Response(200, {}, '[]'); 336 } 337 338 const json = this.serialize(csiVolumes.all()); 339 const namespace = queryParams.namespace || 'default'; 340 return json.filter((volume) => { 341 if (namespace === '*') return true; 342 return namespace === 'default' 343 ? !volume.NamespaceID || volume.NamespaceID === namespace 344 : volume.NamespaceID === namespace; 345 }); 346 }) 347 ); 348 349 this.get( 350 '/volume/:id', 351 withBlockingSupport(function ({ csiVolumes }, { params, queryParams }) { 352 if (!params.id.startsWith('csi/')) { 353 return new Response(404, {}, null); 354 } 355 356 const id = params.id.replace(/^csi\//, ''); 357 const volume = csiVolumes.all().models.find((volume) => { 358 const volumeIsDefault = 359 !volume.namespaceId || volume.namespaceId === 'default'; 360 const qpIsDefault = 361 !queryParams.namespace || queryParams.namespace === 'default'; 362 return ( 363 volume.id === id && 364 (volume.namespaceId === queryParams.namespace || 365 (volumeIsDefault && qpIsDefault)) 366 ); 367 }); 368 369 return volume ? this.serialize(volume) : new Response(404, {}, null); 370 }) 371 ); 372 373 this.get('/plugins', function ({ csiPlugins }, { queryParams }) { 374 if (queryParams.type !== 'csi') { 375 return new Response(200, {}, '[]'); 376 } 377 378 return this.serialize(csiPlugins.all()); 379 }); 380 381 this.get('/plugin/:id', function ({ csiPlugins }, { params }) { 382 if (!params.id.startsWith('csi/')) { 383 return new Response(404, {}, null); 384 } 385 386 const id = params.id.replace(/^csi\//, ''); 387 const volume = csiPlugins.find(id); 388 389 if (!volume) { 390 return new Response(404, {}, null); 391 } 392 393 return this.serialize(volume); 394 }); 395 396 this.get('/namespaces', function ({ namespaces }) { 397 const records = namespaces.all(); 398 399 if (records.length) { 400 return this.serialize(records); 401 } 402 403 return this.serialize([{ Name: 'default' }]); 404 }); 405 406 this.get('/namespace/:id', function ({ namespaces }, { params }) { 407 return this.serialize(namespaces.find(params.id)); 408 }); 409 410 this.get('/agent/members', function ({ agents, regions }) { 411 const firstRegion = regions.first(); 412 return { 413 ServerRegion: firstRegion ? firstRegion.id : null, 414 Members: this.serialize(agents.all()).map(({ member }) => ({ 415 ...member, 416 })), 417 }; 418 }); 419 420 this.get('/agent/self', function ({ agents }) { 421 return agents.first(); 422 }); 423 424 this.get('/agent/monitor', function ({ agents, nodes }, { queryParams }) { 425 const serverId = queryParams.server_id; 426 const clientId = queryParams.client_id; 427 428 if (serverId && clientId) 429 return new Response(400, {}, 'specify a client or a server, not both'); 430 if (serverId && !agents.findBy({ name: serverId })) 431 return new Response(400, {}, 'specified server does not exist'); 432 if (clientId && !nodes.find(clientId)) 433 return new Response(400, {}, 'specified client does not exist'); 434 435 if (queryParams.plain) { 436 return logFrames.join(''); 437 } 438 439 return logEncode(logFrames, logFrames.length - 1); 440 }); 441 442 this.get('/status/leader', function (schema) { 443 return JSON.stringify(findLeader(schema)); 444 }); 445 446 this.get('/acl/tokens', function ({tokens}, req) { 447 return this.serialize(tokens.all()); 448 }); 449 450 this.get('/acl/token/self', function ({ tokens }, req) { 451 const secret = req.requestHeaders['X-Nomad-Token']; 452 const tokenForSecret = tokens.findBy({ secretId: secret }); 453 454 // Return the token if it exists 455 if (tokenForSecret) { 456 return this.serialize(tokenForSecret); 457 } 458 459 // Client error if it doesn't 460 return new Response(400, {}, null); 461 }); 462 463 this.get('/acl/token/:id', function ({ tokens }, req) { 464 const token = tokens.find(req.params.id); 465 const secret = req.requestHeaders['X-Nomad-Token']; 466 const tokenForSecret = tokens.findBy({ secretId: secret }); 467 468 // Return the token only if the request header matches the token 469 // or the token is of type management 470 if ( 471 token.secretId === secret || 472 (tokenForSecret && tokenForSecret.type === 'management') 473 ) { 474 return this.serialize(token); 475 } 476 477 // Return not authorized otherwise 478 return new Response(403, {}, null); 479 }); 480 481 this.post( 482 '/acl/token/onetime/exchange', 483 function ({ tokens }, { requestBody }) { 484 const { OneTimeSecretID } = JSON.parse(requestBody); 485 486 const tokenForSecret = tokens.findBy({ oneTimeSecret: OneTimeSecretID }); 487 488 // Return the token if it exists 489 if (tokenForSecret) { 490 return { 491 Token: this.serialize(tokenForSecret), 492 }; 493 } 494 495 // Forbidden error if it doesn't 496 return new Response(403, {}, null); 497 } 498 ); 499 500 this.get('/acl/policy/:id', function ({ policies, tokens }, req) { 501 const policy = policies.findBy({ name: req.params.id }); 502 const secret = req.requestHeaders['X-Nomad-Token']; 503 const tokenForSecret = tokens.findBy({ secretId: secret }); 504 505 if (req.params.id === 'anonymous') { 506 if (policy) { 507 return this.serialize(policy); 508 } else { 509 return new Response(404, {}, null); 510 } 511 } 512 513 // Return the policy only if the token that matches the request header 514 // includes the policy or if the token that matches the request header 515 // is of type management 516 if ( 517 tokenForSecret && 518 (tokenForSecret.policies.includes(policy) || 519 tokenForSecret.type === 'management') 520 ) { 521 return this.serialize(policy); 522 } 523 524 // Return not authorized otherwise 525 return new Response(403, {}, null); 526 }); 527 528 this.get('/acl/policies', function ({ policies }, req) { 529 return this.serialize(policies.all()); 530 }); 531 532 this.delete('/acl/policy/:id', function (schema, request) { 533 const { id } = request.params; 534 schema.tokens.all().models.filter(token => token.policyIds.includes(id)).forEach(token => { 535 token.update({ policyIds: token.policyIds.filter(pid => pid !== id) }); 536 }); 537 server.db.policies.remove(id); 538 return ''; 539 }); 540 541 this.put('/acl/policy/:id', function (schema, request) { 542 return new Response(200, {}, {}); 543 }); 544 545 this.post('/acl/policy/:id', function (schema, request) { 546 const { Name, Description, Rules } = JSON.parse(request.requestBody); 547 return server.create('policy', { 548 name: Name, 549 description: Description, 550 rules: Rules, 551 }); 552 553 }); 554 555 this.get('/regions', function ({ regions }) { 556 return this.serialize(regions.all()); 557 }); 558 559 this.get('/operator/license', function ({ features }) { 560 const records = features.all(); 561 562 if (records.length) { 563 return { 564 License: { 565 Features: records.models.mapBy('name'), 566 }, 567 }; 568 } 569 570 return new Response(501, {}, null); 571 }); 572 573 const clientAllocationStatsHandler = function ( 574 { clientAllocationStats }, 575 { params } 576 ) { 577 return this.serialize(clientAllocationStats.find(params.id)); 578 }; 579 580 const clientAllocationLog = function (server, { params, queryParams }) { 581 const allocation = server.allocations.find(params.allocation_id); 582 const tasks = allocation.taskStateIds.map((id) => 583 server.taskStates.find(id) 584 ); 585 586 if (!tasks.mapBy('name').includes(queryParams.task)) { 587 return new Response(400, {}, 'must include task name'); 588 } 589 590 if (queryParams.plain) { 591 return logFrames.join(''); 592 } 593 594 return logEncode(logFrames, logFrames.length - 1); 595 }; 596 597 const clientAllocationFSLsHandler = function ( 598 { allocFiles }, 599 { queryParams: { path } } 600 ) { 601 const filterPath = path.endsWith('/') 602 ? path.substr(0, path.length - 1) 603 : path; 604 const files = filesForPath(allocFiles, filterPath); 605 return this.serialize(files); 606 }; 607 608 const clientAllocationFSStatHandler = function ( 609 { allocFiles }, 610 { queryParams: { path } } 611 ) { 612 const filterPath = path.endsWith('/') 613 ? path.substr(0, path.length - 1) 614 : path; 615 616 // Root path 617 if (!filterPath) { 618 return this.serialize({ 619 IsDir: true, 620 ModTime: new Date(), 621 }); 622 } 623 624 // Either a file or a nested directory 625 const file = allocFiles.where({ path: filterPath }).models[0]; 626 return this.serialize(file); 627 }; 628 629 const clientAllocationCatHandler = function ( 630 { allocFiles }, 631 { queryParams } 632 ) { 633 const [file, err] = fileOrError(allocFiles, queryParams.path); 634 635 if (err) return err; 636 return file.body; 637 }; 638 639 const clientAllocationStreamHandler = function ( 640 { allocFiles }, 641 { queryParams } 642 ) { 643 const [file, err] = fileOrError(allocFiles, queryParams.path); 644 645 if (err) return err; 646 647 // Pretender, and therefore Mirage, doesn't support streaming responses. 648 return file.body; 649 }; 650 651 const clientAllocationReadAtHandler = function ( 652 { allocFiles }, 653 { queryParams } 654 ) { 655 const [file, err] = fileOrError(allocFiles, queryParams.path); 656 657 if (err) return err; 658 return file.body.substr(queryParams.offset || 0, queryParams.limit); 659 }; 660 661 const fileOrError = function ( 662 allocFiles, 663 path, 664 message = 'Operation not allowed on a directory' 665 ) { 666 // Root path 667 if (path === '/') { 668 return [null, new Response(400, {}, message)]; 669 } 670 671 const file = allocFiles.where({ path }).models[0]; 672 if (file.isDir) { 673 return [null, new Response(400, {}, message)]; 674 } 675 676 return [file, null]; 677 }; 678 679 // Client requests are available on the server and the client 680 this.put('/client/allocation/:id/restart', function () { 681 return new Response(204, {}, ''); 682 }); 683 684 this.get('/client/allocation/:id/stats', clientAllocationStatsHandler); 685 this.get('/client/fs/logs/:allocation_id', clientAllocationLog); 686 687 this.get('/client/fs/ls/:allocation_id', clientAllocationFSLsHandler); 688 this.get('/client/fs/stat/:allocation_id', clientAllocationFSStatHandler); 689 this.get('/client/fs/cat/:allocation_id', clientAllocationCatHandler); 690 this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler); 691 this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler); 692 693 this.get('/client/stats', function ({ clientStats }, { queryParams }) { 694 const seed = faker.random.number(10); 695 if (seed >= 8) { 696 const stats = clientStats.find(queryParams.node_id); 697 stats.update({ 698 timestamp: Date.now() * 1000000, 699 CPUTicksConsumed: 700 stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }), 701 }); 702 return this.serialize(stats); 703 } else { 704 return new Response(500, {}, null); 705 } 706 }); 707 708 // TODO: in the future, this hack may be replaceable with dynamic host name 709 // support in pretender: https://github.com/pretenderjs/pretender/issues/210 710 HOSTS.forEach((host) => { 711 this.get( 712 `http://${host}/v1/client/allocation/:id/stats`, 713 clientAllocationStatsHandler 714 ); 715 this.get( 716 `http://${host}/v1/client/fs/logs/:allocation_id`, 717 clientAllocationLog 718 ); 719 720 this.get( 721 `http://${host}/v1/client/fs/ls/:allocation_id`, 722 clientAllocationFSLsHandler 723 ); 724 this.get( 725 `http://${host}/v1/client/stat/ls/:allocation_id`, 726 clientAllocationFSStatHandler 727 ); 728 this.get( 729 `http://${host}/v1/client/fs/cat/:allocation_id`, 730 clientAllocationCatHandler 731 ); 732 this.get( 733 `http://${host}/v1/client/fs/stream/:allocation_id`, 734 clientAllocationStreamHandler 735 ); 736 this.get( 737 `http://${host}/v1/client/fs/readat/:allocation_id`, 738 clientAllocationReadAtHandler 739 ); 740 741 this.get(`http://${host}/v1/client/stats`, function ({ clientStats }) { 742 return this.serialize(clientStats.find(host)); 743 }); 744 }); 745 746 this.post( 747 '/search/fuzzy', 748 function ( 749 { allocations, jobs, nodes, taskGroups, csiPlugins }, 750 { requestBody } 751 ) { 752 const { Text } = JSON.parse(requestBody); 753 754 const matchedAllocs = allocations.where((allocation) => 755 allocation.name.includes(Text) 756 ); 757 const matchedGroups = taskGroups.where((taskGroup) => 758 taskGroup.name.includes(Text) 759 ); 760 const matchedJobs = jobs.where((job) => job.name.includes(Text)); 761 const matchedNodes = nodes.where((node) => node.name.includes(Text)); 762 const matchedPlugins = csiPlugins.where((plugin) => 763 plugin.id.includes(Text) 764 ); 765 766 const transformedAllocs = matchedAllocs.models.map((alloc) => ({ 767 ID: alloc.name, 768 Scope: [alloc.namespace || 'default', alloc.id], 769 })); 770 771 const transformedGroups = matchedGroups.models.map((group) => ({ 772 ID: group.name, 773 Scope: [group.job.namespace, group.job.id], 774 })); 775 776 const transformedJobs = matchedJobs.models.map((job) => ({ 777 ID: job.name, 778 Scope: [job.namespace || 'default', job.id], 779 })); 780 781 const transformedNodes = matchedNodes.models.map((node) => ({ 782 ID: node.name, 783 Scope: [node.id], 784 })); 785 786 const transformedPlugins = matchedPlugins.models.map((plugin) => ({ 787 ID: plugin.id, 788 })); 789 790 const truncatedAllocs = transformedAllocs.slice(0, 20); 791 const truncatedGroups = transformedGroups.slice(0, 20); 792 const truncatedJobs = transformedJobs.slice(0, 20); 793 const truncatedNodes = transformedNodes.slice(0, 20); 794 const truncatedPlugins = transformedPlugins.slice(0, 20); 795 796 return { 797 Matches: { 798 allocs: truncatedAllocs, 799 groups: truncatedGroups, 800 jobs: truncatedJobs, 801 nodes: truncatedNodes, 802 plugins: truncatedPlugins, 803 }, 804 Truncations: { 805 allocs: truncatedAllocs.length < truncatedAllocs.length, 806 groups: truncatedGroups.length < transformedGroups.length, 807 jobs: truncatedJobs.length < transformedJobs.length, 808 nodes: truncatedNodes.length < transformedNodes.length, 809 plugins: truncatedPlugins.length < transformedPlugins.length, 810 }, 811 }; 812 } 813 ); 814 815 this.get( 816 '/recommendations', 817 function ( 818 { jobs, namespaces, recommendations }, 819 { queryParams: { job: id, namespace } } 820 ) { 821 if (id) { 822 if (!namespaces.all().length) { 823 namespace = null; 824 } 825 826 const job = jobs.findBy({ id, namespace }); 827 828 if (!job) { 829 return []; 830 } 831 832 const taskGroups = job.taskGroups.models; 833 834 const tasks = taskGroups.reduce((tasks, taskGroup) => { 835 return tasks.concat(taskGroup.tasks.models); 836 }, []); 837 838 const recommendationIds = tasks.reduce((recommendationIds, task) => { 839 return recommendationIds.concat( 840 task.recommendations.models.mapBy('id') 841 ); 842 }, []); 843 844 return recommendations.find(recommendationIds); 845 } else { 846 return recommendations.all(); 847 } 848 } 849 ); 850 851 this.post( 852 '/recommendations/apply', 853 function ({ recommendations }, { requestBody }) { 854 const { Apply, Dismiss } = JSON.parse(requestBody); 855 856 Apply.concat(Dismiss).forEach((id) => { 857 const recommendation = recommendations.find(id); 858 const task = recommendation.task; 859 860 if (Apply.includes(id)) { 861 task.resources[recommendation.resource] = recommendation.value; 862 } 863 recommendation.destroy(); 864 task.save(); 865 }); 866 867 return {}; 868 } 869 ); 870 871 //#region Variables 872 873 this.get('/vars', function (schema, { queryParams: { namespace } }) { 874 if (namespace && namespace !== '*') { 875 return schema.variables.all().filter((v) => v.namespace === namespace); 876 } else { 877 return schema.variables.all(); 878 } 879 }); 880 881 this.get('/var/:id', function ({ variables }, { params }) { 882 return variables.find(params.id); 883 }); 884 885 this.put('/var/:id', function (schema, request) { 886 const { Path, Namespace, Items } = JSON.parse(request.requestBody); 887 if (request.url.includes('cas=') && Path === 'Auto-conflicting Variable') { 888 return new Response( 889 409, 890 {}, 891 { 892 CreateIndex: 65, 893 CreateTime: faker.date.recent(14) * 1000000, // in the past couple weeks 894 Items: { edited_by: 'your_remote_pal' }, 895 ModifyIndex: 2118, 896 ModifyTime: faker.date.recent(0.01) * 1000000, // a few minutes ago 897 Namespace: Namespace, 898 Path: Path, 899 } 900 ); 901 } else { 902 return server.create('variable', { 903 path: Path, 904 namespace: Namespace, 905 items: Items, 906 id: Path, 907 }); 908 } 909 }); 910 911 this.delete('/var/:id', function (schema, request) { 912 const { id } = request.params; 913 server.db.variables.remove(id); 914 return ''; 915 }); 916 917 //#endregion Variables 918 919 //#region Services 920 921 const allocationServiceChecksHandler = function (schema) { 922 let disasters = [ 923 "Moon's haunted", 924 'reticulating splines', 925 'The operation completed unexpectedly', 926 'Ran out of sriracha :(', 927 '¯\\_(ツ)_/¯', 928 '<!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', 929 ]; 930 let fakeChecks = []; 931 schema.serviceFragments.all().models.forEach((frag, iter) => { 932 [...Array(iter)].forEach((check, checkIter) => { 933 const checkOK = faker.random.boolean(); 934 fakeChecks.push({ 935 Check: `check-${checkIter}`, 936 Group: `job-name.${frag.taskGroup?.name}[1]`, 937 Output: checkOK 938 ? 'nomad: http ok' 939 : disasters[Math.floor(Math.random() * disasters.length)], 940 Service: frag.name, 941 Status: checkOK ? 'success' : 'failure', 942 StatusCode: checkOK ? 200 : 400, 943 Task: frag.task?.name, 944 Timestamp: new Date().getTime(), 945 }); 946 }); 947 }); 948 return fakeChecks; 949 }; 950 951 this.get('/job/:id/services', function (schema, { params }) { 952 const { services } = schema; 953 return this.serialize(services.where({ jobId: params.id })); 954 }); 955 956 this.get('/client/allocation/:id/checks', allocationServiceChecksHandler); 957 958 //#endregion Services 959 960 //#region SSO 961 this.get('/acl/auth-methods', function (schema, request) { 962 return schema.authMethods.all(); 963 }); 964 this.post('/acl/oidc/auth-url', (schema, req) => { 965 const {AuthMethod, ClientNonce, RedirectUri, Meta} = JSON.parse(req.requestBody); 966 return new Response(200, {}, { 967 AuthURL: `/ui/oidc-mock?auth_method=${AuthMethod}&client_nonce=${ClientNonce}&redirect_uri=${RedirectUri}&meta=${Meta}` 968 }); 969 }); 970 971 // Simulate an OIDC callback by assuming the code passed is the secret of an existing token, and return that token. 972 this.post('/acl/oidc/complete-auth', function (schema, req) { 973 const code = JSON.parse(req.requestBody).Code; 974 const token = schema.tokens.findBy({ 975 id: code 976 }); 977 978 return new Response(200, {}, { 979 ACLToken: token.secretId 980 }); 981 }, {timing: 1000}); 982 983 984 985 986 //#endregion SSO 987 } 988 989 function filterKeys(object, ...keys) { 990 const clone = copy(object, true); 991 992 keys.forEach((key) => { 993 delete clone[key]; 994 }); 995 996 return clone; 997 } 998 999 // An empty response but not a 204 No Content. This is still a valid JSON 1000 // response that represents a payload with no worthwhile data. 1001 function okEmpty() { 1002 return new Response(200, {}, '{}'); 1003 } 1004 1005 function generateFailedTGAllocs(job, taskGroups) { 1006 const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name'); 1007 1008 let tgNames = ['tg-one', 'tg-two']; 1009 if (taskGroupsFromSpec && taskGroupsFromSpec.length) 1010 tgNames = taskGroupsFromSpec; 1011 if (taskGroups && taskGroups.length) tgNames = taskGroups; 1012 1013 return tgNames.reduce((hash, tgName) => { 1014 hash[tgName] = generateTaskGroupFailures(); 1015 return hash; 1016 }, {}); 1017 }