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