github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/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 default function() { 15 this.timing = 0; // delay for each request, automatically set to 0 during testing 16 17 this.namespace = 'v1'; 18 this.trackRequests = Ember.testing; 19 20 const nomadIndices = {}; // used for tracking blocking queries 21 const server = this; 22 const withBlockingSupport = function(fn) { 23 return function(schema, request) { 24 // Get the original response 25 let { url } = request; 26 url = url.replace(/index=\d+[&;]?/, ''); 27 const response = fn.apply(this, arguments); 28 29 // Get and increment the appropriate index 30 nomadIndices[url] || (nomadIndices[url] = 2); 31 const index = nomadIndices[url]; 32 nomadIndices[url]++; 33 34 // Annotate the response with the index 35 if (response instanceof Response) { 36 response.headers['X-Nomad-Index'] = index; 37 return response; 38 } 39 return new Response(200, { 'x-nomad-index': index }, response); 40 }; 41 }; 42 43 this.get( 44 '/jobs', 45 withBlockingSupport(function({ jobs }, { queryParams }) { 46 const json = this.serialize(jobs.all()); 47 const namespace = queryParams.namespace || 'default'; 48 return json 49 .filter(job => 50 namespace === 'default' 51 ? !job.NamespaceID || job.NamespaceID === namespace 52 : job.NamespaceID === namespace 53 ) 54 .map(job => filterKeys(job, 'TaskGroups', 'NamespaceID')); 55 }) 56 ); 57 58 this.post('/jobs', function(schema, req) { 59 const body = JSON.parse(req.requestBody); 60 61 if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); 62 63 return okEmpty(); 64 }); 65 66 this.post('/jobs/parse', function(schema, req) { 67 const body = JSON.parse(req.requestBody); 68 69 if (!body.JobHCL) 70 return new Response(400, {}, 'JobHCL is a required field on the request payload'); 71 if (!body.Canonicalize) return new Response(400, {}, 'Expected Canonicalize to be true'); 72 73 // Parse the name out of the first real line of HCL to match IDs in the new job record 74 // Regex expectation: 75 // in: job "job-name" { 76 // out: job-name 77 const nameFromHCLBlock = /.+?"(.+?)"/; 78 const jobName = body.JobHCL.trim() 79 .split('\n')[0] 80 .match(nameFromHCLBlock)[1]; 81 82 const job = server.create('job', { id: jobName }); 83 return new Response(200, {}, this.serialize(job)); 84 }); 85 86 this.post('/job/:id/plan', function(schema, req) { 87 const body = JSON.parse(req.requestBody); 88 89 if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); 90 if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true'); 91 92 const FailedTGAllocs = body.Job.Unschedulable && generateFailedTGAllocs(body.Job); 93 94 return new Response( 95 200, 96 {}, 97 JSON.stringify({ FailedTGAllocs, Diff: generateDiff(req.params.id) }) 98 ); 99 }); 100 101 this.get( 102 '/job/:id', 103 withBlockingSupport(function({ jobs }, { params, queryParams }) { 104 const job = jobs.all().models.find(job => { 105 const jobIsDefault = !job.namespaceId || job.namespaceId === 'default'; 106 const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default'; 107 return ( 108 job.id === params.id && 109 (job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault)) 110 ); 111 }); 112 113 return job ? this.serialize(job) : new Response(404, {}, null); 114 }) 115 ); 116 117 this.post('/job/:id', function(schema, req) { 118 const body = JSON.parse(req.requestBody); 119 120 if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); 121 122 return okEmpty(); 123 }); 124 125 this.get( 126 '/job/:id/summary', 127 withBlockingSupport(function({ jobSummaries }, { params }) { 128 return this.serialize(jobSummaries.findBy({ jobId: params.id })); 129 }) 130 ); 131 132 this.get('/job/:id/allocations', function({ allocations }, { params }) { 133 return this.serialize(allocations.where({ jobId: params.id })); 134 }); 135 136 this.get('/job/:id/versions', function({ jobVersions }, { params }) { 137 return this.serialize(jobVersions.where({ jobId: params.id })); 138 }); 139 140 this.get('/job/:id/deployments', function({ deployments }, { params }) { 141 return this.serialize(deployments.where({ jobId: params.id })); 142 }); 143 144 this.get('/job/:id/deployment', function({ deployments }, { params }) { 145 const deployment = deployments.where({ jobId: params.id }).models[0]; 146 return deployment ? this.serialize(deployment) : new Response(200, {}, 'null'); 147 }); 148 149 this.post('/job/:id/periodic/force', function(schema, { params }) { 150 // Create the child job 151 const parent = schema.jobs.find(params.id); 152 153 // Use the server instead of the schema to leverage the job factory 154 server.create('job', 'periodicChild', { 155 parentId: parent.id, 156 namespaceId: parent.namespaceId, 157 namespace: parent.namespace, 158 createAllocations: parent.createAllocations, 159 }); 160 161 return okEmpty(); 162 }); 163 164 this.delete('/job/:id', function(schema, { params }) { 165 const job = schema.jobs.find(params.id); 166 job.update({ status: 'dead' }); 167 return new Response(204, {}, ''); 168 }); 169 170 this.get('/deployment/:id'); 171 this.post('/deployment/promote/:id', function() { 172 return new Response(204, {}, ''); 173 }); 174 175 this.get('/job/:id/evaluations', function({ evaluations }, { params }) { 176 return this.serialize(evaluations.where({ jobId: params.id })); 177 }); 178 179 this.get('/evaluation/:id'); 180 181 this.get('/deployment/allocations/:id', function(schema, { params }) { 182 const job = schema.jobs.find(schema.deployments.find(params.id).jobId); 183 const allocations = schema.allocations.where({ jobId: job.id }); 184 185 return this.serialize(allocations.slice(0, 3)); 186 }); 187 188 this.get('/nodes', function({ nodes }) { 189 const json = this.serialize(nodes.all()); 190 return json; 191 }); 192 193 this.get('/node/:id'); 194 195 this.get('/node/:id/allocations', function({ allocations }, { params }) { 196 return this.serialize(allocations.where({ nodeId: params.id })); 197 }); 198 199 this.get('/allocations'); 200 201 this.get('/allocation/:id'); 202 203 this.post('/allocation/:id/stop', function() { 204 return new Response(204, {}, ''); 205 }); 206 207 this.get('/namespaces', function({ namespaces }) { 208 const records = namespaces.all(); 209 210 if (records.length) { 211 return this.serialize(records); 212 } 213 214 return new Response(501, {}, null); 215 }); 216 217 this.get('/namespace/:id', function({ namespaces }, { params }) { 218 if (namespaces.all().length) { 219 return this.serialize(namespaces.find(params.id)); 220 } 221 222 return new Response(501, {}, null); 223 }); 224 225 this.get('/agent/members', function({ agents, regions }) { 226 const firstRegion = regions.first(); 227 return { 228 ServerRegion: firstRegion ? firstRegion.id : null, 229 Members: this.serialize(agents.all()), 230 }; 231 }); 232 233 this.get('/status/leader', function(schema) { 234 return JSON.stringify(findLeader(schema)); 235 }); 236 237 this.get('/acl/token/self', function({ tokens }, req) { 238 const secret = req.requestHeaders['X-Nomad-Token']; 239 const tokenForSecret = tokens.findBy({ secretId: secret }); 240 241 // Return the token if it exists 242 if (tokenForSecret) { 243 return this.serialize(tokenForSecret); 244 } 245 246 // Client error if it doesn't 247 return new Response(400, {}, null); 248 }); 249 250 this.get('/acl/token/:id', function({ tokens }, req) { 251 const token = tokens.find(req.params.id); 252 const secret = req.requestHeaders['X-Nomad-Token']; 253 const tokenForSecret = tokens.findBy({ secretId: secret }); 254 255 // Return the token only if the request header matches the token 256 // or the token is of type management 257 if (token.secretId === secret || (tokenForSecret && tokenForSecret.type === 'management')) { 258 return this.serialize(token); 259 } 260 261 // Return not authorized otherwise 262 return new Response(403, {}, null); 263 }); 264 265 this.get('/acl/policy/:id', function({ policies, tokens }, req) { 266 const policy = policies.find(req.params.id); 267 const secret = req.requestHeaders['X-Nomad-Token']; 268 const tokenForSecret = tokens.findBy({ secretId: secret }); 269 270 // Return the policy only if the token that matches the request header 271 // includes the policy or if the token that matches the request header 272 // is of type management 273 if ( 274 tokenForSecret && 275 (tokenForSecret.policies.includes(policy) || tokenForSecret.type === 'management') 276 ) { 277 return this.serialize(policy); 278 } 279 280 // Return not authorized otherwise 281 return new Response(403, {}, null); 282 }); 283 284 this.get('/regions', function({ regions }) { 285 return this.serialize(regions.all()); 286 }); 287 288 const clientAllocationStatsHandler = function({ clientAllocationStats }, { params }) { 289 return this.serialize(clientAllocationStats.find(params.id)); 290 }; 291 292 const clientAllocationLog = function(server, { params, queryParams }) { 293 const allocation = server.allocations.find(params.allocation_id); 294 const tasks = allocation.taskStateIds.map(id => server.taskStates.find(id)); 295 296 if (!tasks.mapBy('name').includes(queryParams.task)) { 297 return new Response(400, {}, 'must include task name'); 298 } 299 300 if (queryParams.plain) { 301 return logFrames.join(''); 302 } 303 304 return logEncode(logFrames, logFrames.length - 1); 305 }; 306 307 // Client requests are available on the server and the client 308 this.put('/client/allocation/:id/restart', function() { 309 return new Response(204, {}, ''); 310 }); 311 312 this.get('/client/allocation/:id/stats', clientAllocationStatsHandler); 313 this.get('/client/fs/logs/:allocation_id', clientAllocationLog); 314 315 this.get('/client/stats', function({ clientStats }, { queryParams }) { 316 const seed = Math.random(); 317 if (seed > 0.8) { 318 const stats = clientStats.find(queryParams.node_id); 319 stats.update({ 320 timestamp: Date.now() * 1000000, 321 CPUTicksConsumed: stats.CPUTicksConsumed + (Math.random() * 20 - 10), 322 }); 323 return this.serialize(stats); 324 } else { 325 return new Response(500, {}, null); 326 } 327 }); 328 329 // TODO: in the future, this hack may be replaceable with dynamic host name 330 // support in pretender: https://github.com/pretenderjs/pretender/issues/210 331 HOSTS.forEach(host => { 332 this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler); 333 this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog); 334 335 this.get(`http://${host}/v1/client/stats`, function({ clientStats }) { 336 return this.serialize(clientStats.find(host)); 337 }); 338 }); 339 } 340 341 function filterKeys(object, ...keys) { 342 const clone = copy(object, true); 343 344 keys.forEach(key => { 345 delete clone[key]; 346 }); 347 348 return clone; 349 } 350 351 // An empty response but not a 204 No Content. This is still a valid JSON 352 // response that represents a payload with no worthwhile data. 353 function okEmpty() { 354 return new Response(200, {}, '{}'); 355 } 356 357 function generateFailedTGAllocs(job, taskGroups) { 358 const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name'); 359 360 let tgNames = ['tg-one', 'tg-two']; 361 if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec; 362 if (taskGroups && taskGroups.length) tgNames = taskGroups; 363 364 return tgNames.reduce((hash, tgName) => { 365 hash[tgName] = generateTaskGroupFailures(); 366 return hash; 367 }, {}); 368 }