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
|
@@ -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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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 {
|
|
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
|
|
60
|
+
const updatedPipeline = await updatePipelineAdmins(
|
|
61
|
+
{
|
|
62
|
+
id,
|
|
63
|
+
scmContext: payloadScmContext,
|
|
64
|
+
usernames
|
|
65
|
+
},
|
|
66
|
+
request.server
|
|
67
|
+
);
|
|
61
68
|
|
|
62
|
-
|
|
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({
|