screwdriver-api 8.0.124 → 8.0.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screwdriver-api",
3
- "version": "8.0.124",
3
+ "version": "8.0.126",
4
4
  "description": "API server for the Screwdriver.cd service",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -123,7 +123,7 @@
123
123
  "screwdriver-executor-queue": "^6.0.0",
124
124
  "screwdriver-executor-router": "^5.0.0",
125
125
  "screwdriver-logger": "^3.0.0",
126
- "screwdriver-models": "^33.0.0",
126
+ "screwdriver-models": "^33.2.0",
127
127
  "screwdriver-notifications-email": "^5.0.0",
128
128
  "screwdriver-notifications-slack": "^7.0.0",
129
129
  "screwdriver-request": "^3.0.0",
@@ -7,6 +7,7 @@ const ndjson = require('ndjson');
7
7
  const request = require('screwdriver-request');
8
8
  const schema = require('screwdriver-data-schema');
9
9
  const { v4: uuidv4 } = require('uuid');
10
+ const { Readable } = require('stream');
10
11
 
11
12
  const MAX_LINES_SMALL = 100;
12
13
  const MAX_LINES_BIG = 1000;
@@ -208,6 +209,75 @@ function durationTime(sourceTimestamp, targetTimestamp) {
208
209
  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
209
210
  }
210
211
 
212
+ /**
213
+ * Generates a stream of log lines for a build's step.
214
+ * @param {Object} config
215
+ * @param {string} config.baseUrl URL to load from (without the .$PAGE)
216
+ * @param {string} config.authToken Bearer Token to be passed to the Store
217
+ * @param {number} config.maxLines Maximum number of lines per page
218
+ * @param {number} config.totalPages Total number of pages to fetch
219
+ * @param {number} config.timestamp TimeStamp in unix milliseconds format
220
+ * @param {string} config.timestampFormat Format of the timestamp
221
+ * @param {string} config.timezone Timezone of timestamp
222
+ * @param {Object} config.buildModel Build data
223
+ * @param {Object} config.stepModel Step data
224
+ * @returns {AsyncGenerator<string>} An async generator yielding log lines
225
+ */
226
+ async function* generateLog({
227
+ baseUrl,
228
+ authToken,
229
+ maxLines,
230
+ totalPages,
231
+ timestamp,
232
+ timestampFormat,
233
+ timezone,
234
+ buildModel,
235
+ stepModel
236
+ }) {
237
+ try {
238
+ const buildTime = timestamp ? new Date(buildModel.startTime).getTime() : 0;
239
+ const stepTime = timestamp ? new Date(stepModel.startTime).getTime() : 0;
240
+
241
+ for (let page = 0; page < totalPages; page += 1) {
242
+ const lines = await fetchLog({
243
+ baseUrl,
244
+ authToken,
245
+ page,
246
+ sort: 'ascending',
247
+ linesFrom: page * maxLines
248
+ });
249
+ let output = '';
250
+
251
+ for (const line of lines) {
252
+ if (timestamp) {
253
+ switch (timestampFormat) {
254
+ case 'full-time':
255
+ output += `${unixToFullTime(line.t, timezone)}\t${line.m}\n`;
256
+ break;
257
+ case 'simple-time':
258
+ output += `${unixToSimpleTime(line.t, timezone)}\t${line.m}\n`;
259
+ break;
260
+ case 'elapsed-build':
261
+ output += `${durationTime(buildTime, line.t)}\t${line.m}\n`;
262
+ break;
263
+ case 'elapsed-step':
264
+ output += `${durationTime(stepTime, line.t)}\t${line.m}\n`;
265
+ break;
266
+ default:
267
+ throw boom.badRequest('Unexpected timestampFormat parameter');
268
+ }
269
+ } else {
270
+ output += `${line.m}\n`;
271
+ }
272
+ }
273
+ yield output;
274
+ }
275
+ } catch (err) {
276
+ logger.error(`Failed to stream logs for build ${buildModel.id}: ${err.message}`);
277
+ throw err;
278
+ }
279
+ }
280
+
211
281
  module.exports = config => ({
212
282
  method: 'GET',
213
283
  path: '/builds/{id}/steps/{name}/logs',
@@ -276,84 +346,54 @@ module.exports = config => ({
276
346
 
277
347
  const { sort, type, timestamp, timezone, timestampFormat } = req.query;
278
348
 
279
- let pagesToLoad = req.query.pages;
280
- let linesFrom = req.query.from;
281
-
282
- if (type === 'download' && isDone) {
283
- // 100 lines per page
284
- pagesToLoad = Math.ceil(stepModel.lines / 100);
285
- linesFrom = 0;
286
- }
349
+ const pagesToLoad = req.query.pages;
350
+ const linesFrom = req.query.from;
287
351
 
288
- return getMaxLines({ baseUrl, authToken })
289
- .then(maxLines =>
290
- loadLines({
352
+ return getMaxLines({ baseUrl, authToken }).then(maxLines => {
353
+ if (type !== 'download') {
354
+ return loadLines({
291
355
  baseUrl,
292
356
  linesFrom,
293
357
  authToken,
294
358
  pagesToLoad,
295
359
  sort,
296
360
  maxLines
297
- })
298
- )
299
- .then(([lines, morePages]) => {
300
- if (type !== 'download') {
301
- return h.response(lines).header('X-More-Data', (morePages || !isDone).toString());
302
- }
303
-
304
- let res = '';
305
-
306
- if (timestamp) {
307
- const buildTime = new Date(buildModel.startTime).getTime();
308
- const stepTime = new Date(stepModel.startTime).getTime();
309
-
310
- switch (timestampFormat) {
311
- case 'full-time':
312
- for (const line of lines) {
313
- res += `${unixToFullTime(line.t, timezone)}\t${line.m}\n`;
314
- }
315
- break;
316
- case 'simple-time':
317
- for (const line of lines) {
318
- res += `${unixToSimpleTime(line.t, timezone)}\t${line.m}\n`;
319
- }
320
- break;
321
- case 'elapsed-build':
322
- for (const line of lines) {
323
- const duration = durationTime(buildTime, line.t);
324
-
325
- res += `${duration}\t${line.m}\n`;
326
- }
327
- break;
328
- case 'elapsed-step':
329
- for (const line of lines) {
330
- const duration = durationTime(stepTime, line.t);
331
-
332
- res += `${duration}\t${line.m}\n`;
333
- }
334
- break;
335
- default:
336
- throw boom.badRequest('Unexpected timestampFormat parameter');
337
- }
338
- } else {
339
- for (const line of lines) {
340
- res += `${line.m}\n`;
361
+ }).then(([lines, morePages]) => {
362
+ const { error } = schema.api.loglines.output.validate(lines);
363
+
364
+ if (error) {
365
+ throw error;
341
366
  }
342
- }
343
367
 
344
- return h
345
- .response(res)
346
- .type('text/plain')
347
- .header('content-disposition', `attachment; filename="${stepName}-log.txt"`);
368
+ return h.response(lines).header('X-More-Data', (morePages || !isDone).toString());
369
+ });
370
+ }
371
+ const totalPages = Math.ceil(stepModel.lines / maxLines);
372
+
373
+ const logStream = generateLog({
374
+ baseUrl,
375
+ authToken,
376
+ maxLines,
377
+ totalPages,
378
+ timestamp,
379
+ timestampFormat,
380
+ timezone,
381
+ buildModel,
382
+ stepModel
348
383
  });
384
+
385
+ const responseStream = Readable.from(logStream, { objectMode: false });
386
+
387
+ return h
388
+ .response(responseStream)
389
+ .type('text/plain')
390
+ .header('content-disposition', `attachment; filename="${stepName}-log.txt"`);
391
+ });
349
392
  })
350
393
  .catch(err => {
351
394
  throw err;
352
395
  });
353
396
  },
354
- response: {
355
- schema: schema.api.loglines.output
356
- },
357
397
  validate: {
358
398
  params: schema.api.loglines.params,
359
399
  query: schema.api.loglines.query
@@ -63,6 +63,7 @@ module.exports = () => ({
63
63
  admins: {
64
64
  [username]: true
65
65
  },
66
+ adminUserIds: [user.id],
66
67
  scmContext,
67
68
  scmUri
68
69
  };