screwdriver-api 8.0.50 → 8.0.52

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.50",
3
+ "version": "8.0.52",
4
4
  "description": "API server for the Screwdriver.cd service",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -140,6 +140,74 @@ async function loadLines({ baseUrl, linesFrom, authToken, pagesToLoad = 10, sort
140
140
  return [lines, morePages];
141
141
  }
142
142
 
143
+ /**
144
+ * Convert unix milliseconds to ISO 8610 with timezone format
145
+ * @param {number} timestamp TimeStamp in unix milliseconds format
146
+ * @param {string} timeZone Timezone of timestamp
147
+ * @returns {string} Datetime in ISO 8610 with timezone format (e.g., YYYY-MM-DDThh:mm:ss.sssZ, YYYY-MM-DDThh:mm:ss.sss+09:00)
148
+ */
149
+ function unixToFullTime(timestamp, timeZone) {
150
+ const date = new Date(timestamp);
151
+ const formatter = new Intl.DateTimeFormat('en-US', {
152
+ timeZone,
153
+ year: 'numeric',
154
+ month: '2-digit',
155
+ day: '2-digit',
156
+ hour: '2-digit',
157
+ minute: '2-digit',
158
+ second: '2-digit',
159
+ fractionalSecondDigits: 3,
160
+ hour12: false,
161
+ timeZoneName: 'longOffset'
162
+ });
163
+ const { year, day, month, hour, minute, second, fractionalSecond, timeZoneName } = Object.fromEntries(
164
+ formatter.formatToParts(date).map(({ type, value }) => [type, value])
165
+ );
166
+
167
+ const offsetMatch = timeZoneName.match(/GMT(.*)/)[1];
168
+
169
+ const timezoneOffset = offsetMatch === '' ? 'Z' : offsetMatch;
170
+
171
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}.${fractionalSecond}${timezoneOffset}`;
172
+ }
173
+
174
+ /**
175
+ * Convert unix milliseconds to Datetime with Timezone
176
+ * @param {number} timestamp TimeStamp in unix milliseconds format
177
+ * @param {string} timeZone Timezone of timestamp
178
+ * @returns {string} Datetime in hh:mm:ss format
179
+ */
180
+ function unixToSimpleTime(timestamp, timeZone) {
181
+ const date = new Date(timestamp);
182
+ const options = {
183
+ timeZone,
184
+ hour: '2-digit',
185
+ minute: '2-digit',
186
+ second: '2-digit',
187
+ hour12: false
188
+ };
189
+
190
+ return date.toLocaleString(undefined, options);
191
+ }
192
+
193
+ /**
194
+ * Convert to target and source unix milliseconds duration
195
+ * @param {number} sourceTimestamp Source timeStamp in unix milliseconds format
196
+ * @param {number} targetTimestamp Target timeStamp in unix milliseconds format
197
+ * @returns {string} Duration in hh:mm:ss format
198
+ */
199
+ function durationTime(sourceTimestamp, targetTimestamp) {
200
+ const differenceInMilliSeconds = targetTimestamp - sourceTimestamp;
201
+ const differenceInSeconds = Math.floor(differenceInMilliSeconds / 1000);
202
+ const differenceInSecondsMod = differenceInSeconds % 3600;
203
+
204
+ const hours = Math.floor(differenceInSeconds / 3600);
205
+ const minutes = Math.floor(differenceInSecondsMod / 60);
206
+ const seconds = (differenceInSecondsMod % 60).toString().padStart(2, '0');
207
+
208
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
209
+ }
210
+
143
211
  module.exports = config => ({
144
212
  method: 'GET',
145
213
  path: '/builds/{id}/steps/{name}/logs',
@@ -158,6 +226,7 @@ module.exports = config => ({
158
226
  const { stepFactory, buildFactory, eventFactory } = req.server.app;
159
227
  const buildId = req.params.id;
160
228
  const stepName = req.params.name;
229
+ let buildModel;
161
230
 
162
231
  return buildFactory
163
232
  .get(buildId)
@@ -165,6 +234,7 @@ module.exports = config => ({
165
234
  if (!build) {
166
235
  throw boom.notFound('Build does not exist');
167
236
  }
237
+ buildModel = build;
168
238
 
169
239
  return eventFactory.get(build.eventId);
170
240
  })
@@ -203,7 +273,9 @@ module.exports = config => ({
203
273
  jwtid: uuidv4()
204
274
  }
205
275
  );
206
- const { sort, type } = req.query;
276
+
277
+ const { sort, type, timestamp, timezone, timestampFormat } = req.query;
278
+
207
279
  let pagesToLoad = req.query.pages;
208
280
  let linesFrom = req.query.from;
209
281
 
@@ -231,8 +303,42 @@ module.exports = config => ({
231
303
 
232
304
  let res = '';
233
305
 
234
- for (let i = 0; i < lines.length; i += 1) {
235
- res = `${res}${lines[i].m}\n`;
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`;
341
+ }
236
342
  }
237
343
 
238
344
  return h
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const boom = require('@hapi/boom');
4
+ const schema = require('screwdriver-data-schema');
5
+ const joi = require('joi');
6
+ const idSchema = schema.models.pipeline.base.extract('id');
7
+ const scmContextSchema = schema.models.pipeline.base.extract('scmContext');
8
+ const usernameSchema = schema.models.user.base.extract('username');
9
+ const { updatePipelineAdmins } = require('./helper/updateAdmins');
10
+
11
+ module.exports = () => ({
12
+ method: 'PUT',
13
+ path: '/pipelines/updateAdmins',
14
+ options: {
15
+ description: 'Update admins for a collection of pipelines',
16
+ notes: 'Update the admins for a collection of pipelines',
17
+ tags: ['api', 'pipelines'],
18
+ auth: {
19
+ strategies: ['token'],
20
+ scope: ['user', '!guest']
21
+ },
22
+ handler: async (request, h) => {
23
+ const { scmContext, username, scmUserId } = request.auth.credentials;
24
+ const { payload } = request;
25
+
26
+ const { bannerFactory } = request.server.app;
27
+
28
+ // Check token permissions
29
+ // Only SD cluster admins can update the admins
30
+ const scmDisplayName = bannerFactory.scm.getDisplayName({ scmContext });
31
+
32
+ const adminDetails = request.server.plugins.banners.screwdriverAdminDetails(
33
+ username,
34
+ scmDisplayName,
35
+ scmUserId
36
+ );
37
+
38
+ if (!adminDetails.isAdmin) {
39
+ throw boom.forbidden(
40
+ `User ${username} does not have Screwdriver administrative privileges to update the admins for pipelines`
41
+ );
42
+ }
43
+
44
+ await Promise.all(
45
+ payload.map(e => {
46
+ return updatePipelineAdmins(e, request.server);
47
+ })
48
+ );
49
+
50
+ return h.response().code(204);
51
+ },
52
+ validate: {
53
+ payload: joi
54
+ .array()
55
+ .items(
56
+ joi.object({
57
+ id: idSchema.required(),
58
+ scmContext: scmContextSchema.required(),
59
+ usernames: joi.array().items(usernameSchema).min(1).max(50).required()
60
+ })
61
+ )
62
+ .min(1)
63
+ }
64
+ }
65
+ });
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const boom = require('@hapi/boom');
4
+ const logger = require('screwdriver-logger');
5
+
6
+ /**
7
+ * @typedef {import('screwdriver-models/lib/pipeline')} Pipeline
8
+ */
9
+
10
+ /**
11
+ * Adds the users as admins for the specified pipeline
12
+ *
13
+ * @method updateBuildAndTriggerDownstreamJobs
14
+ * @param {Object} config
15
+ * @param {Number} config.id Pipeline id
16
+ * @param {Array} [config.usernames] List of usernames to be added as admins to the pipeline
17
+ * @param {String} config.scmContext SCM Context the users are associated with
18
+ * @param {Object} server
19
+ * @returns {Promise<Pipeline>} Updated pipeline
20
+ */
21
+ async function updatePipelineAdmins(config, server) {
22
+ const { pipelineFactory, userFactory } = server.app;
23
+ const { id, scmContext, usernames } = config;
24
+
25
+ const pipeline = await pipelineFactory.get({ id });
26
+
27
+ // check if pipeline exists
28
+ if (!pipeline) {
29
+ throw boom.notFound(`Pipeline ${id} does not exist`);
30
+ }
31
+ if (pipeline.state === 'DELETING') {
32
+ throw boom.conflict('This pipeline is being deleted.');
33
+ }
34
+
35
+ const users = await userFactory.list({
36
+ params: {
37
+ username: usernames,
38
+ scmContext
39
+ }
40
+ });
41
+
42
+ const adminUsernamesForUpdate = [];
43
+ const newAdmins = new Set(pipeline.adminUserIds);
44
+
45
+ users.forEach(user => {
46
+ newAdmins.add(user.id);
47
+ adminUsernamesForUpdate.push(user.username);
48
+ });
49
+
50
+ pipeline.adminUserIds = Array.from(newAdmins);
51
+
52
+ try {
53
+ const updatedPipeline = await pipeline.update();
54
+
55
+ logger.info(`Updated admins ${adminUsernamesForUpdate} for pipeline(id=${id})`);
56
+
57
+ return updatedPipeline;
58
+ } catch (err) {
59
+ logger.error(`Failed to update admins ${adminUsernamesForUpdate} for pipeline(id=${id}): ${err.message}`);
60
+ throw boom.internal(`Failed to update admins for pipeline ${id}`);
61
+ }
62
+ }
63
+
64
+ module.exports = {
65
+ updatePipelineAdmins
66
+ };
@@ -45,6 +45,7 @@ const removeTemplateVersionRoute = require('./templates/removeVersion');
45
45
  const updateTrustedRoute = require('./templates/updateTrusted');
46
46
  const updateBuildCluster = require('./updateBuildCluster');
47
47
  const updateAdminsRoute = require('./updateAdmins');
48
+ const batchUpdateAdminsRoute = require('./batchUpdateAdmins');
48
49
 
49
50
  /**
50
51
  * Pipeline API Plugin
@@ -280,7 +281,8 @@ const pipelinesPlugin = {
280
281
  removeTemplateVersionRoute(),
281
282
  updateTrustedRoute(),
282
283
  updateBuildCluster(),
283
- updateAdminsRoute()
284
+ updateAdminsRoute(),
285
+ batchUpdateAdminsRoute()
284
286
  ]);
285
287
  }
286
288
  };
@@ -3,8 +3,8 @@
3
3
  const boom = require('@hapi/boom');
4
4
  const joi = require('joi');
5
5
  const schema = require('screwdriver-data-schema');
6
- const logger = require('screwdriver-logger');
7
6
  const idSchema = schema.models.pipeline.base.extract('id');
7
+ const { updatePipelineAdmins } = require('./helper/updateAdmins');
8
8
 
9
9
  module.exports = () => ({
10
10
  method: 'PUT',
@@ -31,7 +31,7 @@ module.exports = () => ({
31
31
  throw boom.badRequest(`Payload must contain scmContext`);
32
32
  }
33
33
 
34
- const { pipelineFactory, bannerFactory, userFactory } = request.server.app;
34
+ const { bannerFactory } = request.server.app;
35
35
 
36
36
  // Check token permissions
37
37
  if (isPipeline) {
@@ -57,45 +57,16 @@ module.exports = () => ({
57
57
  }
58
58
  }
59
59
 
60
- const pipeline = await pipelineFactory.get({ id });
60
+ const updatedPipeline = await updatePipelineAdmins(
61
+ {
62
+ id,
63
+ scmContext: payloadScmContext,
64
+ usernames
65
+ },
66
+ request.server
67
+ );
61
68
 
62
- // check if pipeline exists
63
- if (!pipeline) {
64
- throw boom.notFound(`Pipeline ${id} does not exist`);
65
- }
66
- if (pipeline.state === 'DELETING') {
67
- throw boom.conflict('This pipeline is being deleted.');
68
- }
69
-
70
- const users = await userFactory.list({
71
- params: {
72
- username: usernames,
73
- scmContext: payloadScmContext
74
- }
75
- });
76
-
77
- const adminUsernamesForUpdate = [];
78
- const newAdmins = new Set(pipeline.adminUserIds);
79
-
80
- users.forEach(user => {
81
- newAdmins.add(user.id);
82
- adminUsernamesForUpdate.push(user.username);
83
- });
84
-
85
- pipeline.adminUserIds = Array.from(newAdmins);
86
-
87
- try {
88
- const result = await pipeline.update();
89
-
90
- logger.info(`Updated admins ${adminUsernamesForUpdate} for pipeline(id=${id})`);
91
-
92
- return h.response(result.toJson()).code(200);
93
- } catch (err) {
94
- logger.error(
95
- `Failed to update admins ${adminUsernamesForUpdate} for pipeline(id=${id}): ${err.message}`
96
- );
97
- throw boom.internal(`Failed to update admins for pipeline ${id}`);
98
- }
69
+ return h.response(updatedPipeline.toJson()).code(200);
99
70
  },
100
71
  validate: {
101
72
  params: joi.object({