-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
image-metadata-helper.ts
402 lines (344 loc) · 14.3 KB
/
image-metadata-helper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
import * as tl from "azure-pipelines-task-lib/task";
import * as util from "util";
import * as yaml from 'js-yaml';
const matchPatternForImageName = new RegExp(/\:\/\/(.+?)\@/);
const matchPatternForDigest = new RegExp(/\@sha256\:(.+)/);
const matchPatternForFileArgument = new RegExp(/-f\s|-filename\s/);
const matchPatternForServerUrl = new RegExp(/https\:\/\/(.+)/);
const matchPatternForSource = new RegExp(/source:(.+)/ig);
const matchPatternForChartPath = new RegExp(/chart path:(.+)/i);
const orgUrl = tl.getVariable('System.TeamFoundationCollectionUri');
const build = "build";
const hostType = tl.getVariable("System.HostType").toLowerCase();
const isBuild = hostType === build;
const deploymentTypes: string[] = ["deployment", "replicaset", "daemonset", "pod", "statefulset"];
const workingDirectory = tl.getVariable("System.DefaultWorkingDirectory");
const branch = tl.getVariable("Build.SourceBranchName") || tl.getVariable("Build.SourceBranch");
const repositoryProvider = tl.getVariable("Build.Repository.Provider");
const repositoryUrl = tl.getVariable("Build.Repository.Uri");
const pipelineUrlLabel = "Pipeline_Url";
const clusterUrlLabel = "Cluster_Url";
const manifestUrlLabel = "Manifest_Url";
// ToDo: Add UTs for public methods
export function getDeploymentMetadata(deploymentObject: any, allPods: any, deploymentStrategy: string, clusterInfo: any, manifestUrls: string[]): any {
let imageIds: string[] = [];
let containers = [];
try {
let kind: string = deploymentObject.kind;
if (isPodEntity(kind)) {
containers = deploymentObject.spec.containers;
}
else {
containers = deploymentObject.spec.template.spec.containers;
}
if (containers && containers.length > 0) {
containers.forEach(container => {
// Filter all pods using the container names in this deployment,
// and get the imageIds from pod status
imageIds = getImageIdsForPodsInDeployment(container.name, allPods.items);
});
}
}
catch (e) {
// Don't fail the task if the image ID extraction fails
console.log("Image Ids extraction failed with exception: " + e);
}
let name: string = deploymentObject.metadata && deploymentObject.metadata.name ? deploymentObject.metadata.name : "";
let relatedUrls = [];
let pipelineUrl = getPipelineUrl();
if (pipelineUrl) {
relatedUrls.push(pipelineUrl);
}
let clusterUrl = getServerUrl(clusterInfo);
if (clusterUrl) {
relatedUrls.push(clusterUrl);
}
if (manifestUrls.length > 0) {
for (const url of manifestUrls) {
let relatedUrl = getRelatedUrl(url, manifestUrlLabel);
relatedUrls.push(relatedUrl);
}
}
const metadataDetails = {
"Name": name,
"Description": getDescription(),
"RelatedUrl": relatedUrls,
"ResourceUri": imageIds,
"UserEmail": getUserEmail(),
"Config": deploymentStrategy,
"Address": getEnvironmentResourceAddress(clusterUrl),
"Platform": getPlatform(),
"PipelineRunInfo": getPipelineRunInfo()
};
return metadataDetails;
}
export function getImageIdsForPodsInDeployment(containerName: string, pods: any[]): string[] {
// The image name in parent.spec.template.spec.containers and in pod.status.containerStatuses is not a constant, example it is redis in former, and redis:latest in latter
// Hence filtering the pods on the basis of container name which is a constant
let imageIds: string[] = [];
pods.forEach(pod => {
const podStatus = pod.status;
podStatus.containerStatuses.forEach(status => {
if (status.name.toLowerCase() === containerName.toLowerCase()) {
if (status.imageID) {
imageIds.push(getImageResourceUrl(status.imageID));
}
}
});
});
return imageIds;
}
export function getImageIdsForPod(pod: any): string[] {
let imageIds: string[] = [];
const podStatus = pod.status;
podStatus.containerStatuses.forEach(status => {
if (status.imageID) {
imageIds.push(getImageResourceUrl(status.imageID));
}
});
return imageIds;
}
export function getImageResourceUrl(imageId: string): string {
const sha256Text = "@sha256:";
const separator = "://";
let indexOfSeparator = imageId.indexOf(separator);
let image = indexOfSeparator >= 0 ? imageId.substr(indexOfSeparator + separator.length) : imageId;
const digest = getImageResourceUrlParameter(imageId, matchPatternForDigest);
let match = image.match(/^(?:([^\/]+)\/)?(?:([^\/]+)\/)?([^@:\/]+)(?:[@:](.+))?$/);
if (!match) {
return "";
}
let registry = match[1];
let imgNamespace = match[2];
let repository = match[3];
if (!imgNamespace && registry && !/[:.]/.test(registry)) {
imgNamespace = registry;
registry = "docker.io";
}
if (!imgNamespace && !registry) {
registry = "docker.io";
imgNamespace = "library";
}
registry = registry ? registry + "/" : "";
imgNamespace = imgNamespace ? imgNamespace + "/" : "";
return util.format("https://%s%s%s%s%s", registry, imgNamespace, repository, sha256Text, digest);
}
export function getImageResourceUrlParameter(imageId: string, matchPattern: RegExp): string {
const imageMatch = imageId.match(matchPattern);
if (imageMatch && imageMatch.length >= 1) {
return imageMatch[1];
}
return "";
}
function getUserEmail(): string {
const build = "build";
const buildReason = "schedule";
const hostType = tl.getVariable("System.HostType").toLowerCase();
let userEmail: string = "";
if (hostType === build && tl.getVariable("Build.Reason").toLowerCase() !== buildReason) {
userEmail = tl.getVariable("Build.RequestedForEmail");
}
else {
userEmail = tl.getVariable("Release.RequestedForEmail");
}
return userEmail;
}
function getDescription(): string {
// Todo: Should we have a particular description with deployment details?
const release = "release";
const hostType = tl.getVariable("System.HostType").toLowerCase();
const description: string = hostType === release ? tl.getVariable("Release.ReleaseDescription") : "";
return description;
}
function getEnvironmentResourceAddress(clusterUrl: any): string {
const environmentName = tl.getVariable("Environment.Name");
const environmentResourceName = tl.getVariable("Environment.ResourceName");
if (!environmentName && !environmentResourceName) {
if (clusterUrl && clusterUrl["url"]) {
return clusterUrl["url"];
}
return "";
}
return util.format("%s/%s", environmentName, environmentResourceName);
}
function getPipelineUrl(): { [key: string]: string } {
let pipelineUrl = "";
const pipelineId = tl.getVariable("System.DefinitionId");
if (isBuild) {
pipelineUrl = orgUrl + tl.getVariable("System.TeamProject") + "/_build?definitionId=" + pipelineId;
}
else {
pipelineUrl = orgUrl + tl.getVariable("System.TeamProject") + "/_release?definitionId=" + pipelineId;
}
return getRelatedUrl(pipelineUrl, pipelineUrlLabel);
}
function getServerUrl(clusterInfo: any): { [key: string]: string } {
let serverUrl: string = "";
let serverUrlMatch = clusterInfo.match(matchPatternForServerUrl);
if (serverUrlMatch && serverUrlMatch.length >= 1) {
serverUrl = serverUrlMatch[0];
}
return getRelatedUrl(serverUrl, clusterUrlLabel);
}
export function extractManifestsFromHelmOutput(helmOutput: string): any {
let manifestObjects = [];
let manifestFiles = "";
// The output stream contains the manifest file between the manifest and last deployed fields
const manifestString = "manifest:";
const lastDeployedString = "last deployed:";
let indexOfManifests = helmOutput.toLowerCase().indexOf(manifestString);
let indexOfLastDeployed = helmOutput.toLowerCase().indexOf(lastDeployedString);
if (indexOfManifests >= 0 && indexOfLastDeployed >= 0) {
manifestFiles = helmOutput.substring(indexOfManifests + manifestString.length, indexOfLastDeployed);
}
if (manifestFiles) {
// Each of the source manifests is separated in output stream via string '---'
const files = manifestFiles.split("---");
files.forEach(file => {
file = file.trim();
if (file) {
const parsedObject = yaml.safeLoad(file);
manifestObjects.push(parsedObject);
}
});
}
return manifestObjects;
}
export function getManifestFileUrlsFromArgumentsInput(fileArgs: string): string[] {
let manifestFileUrls: string[] = [];
const filePathMatch: string[] = fileArgs ? fileArgs.split(matchPatternForFileArgument) : [];
if (filePathMatch && filePathMatch.length > 0) {
filePathMatch.forEach(manifestPath => {
if (!!manifestPath) {
if (manifestPath.startsWith("http") || manifestPath.startsWith("https:")) {
manifestFileUrls.push(manifestPath);
}
else {
manifestFileUrls.push(...getManifestUrls([manifestPath]));
}
}
});
}
return manifestFileUrls;
}
export function getManifestFileUrlsFromHelmOutput(helmOutput: string): string[] {
const chartType = tl.getInput("chartType", true);
// Raw github links are supported only for chart names not chart paths
if (chartType === "Name") {
const chartName = tl.getInput("chartName", true);
if (chartName.startsWith("http:") || chartName.startsWith("https:")) {
return [chartName];
}
}
let manifestFilePaths: string[] = [];
// Extract the chart directory
const directoryName = getChartDirectoryName(helmOutput);
// Extract all source paths; source path example - # Source: MyChart/templates/pod.yaml
const filePathMatches = helmOutput.match(matchPatternForSource);
if (filePathMatches && filePathMatches.length >= 1) {
filePathMatches.forEach(filePathMatch => {
// Strip the Chart name from source path to get the template path
let indexOfTemplate = filePathMatch.toLowerCase().indexOf("templates");
const templatePath = indexOfTemplate >= 0 ? filePathMatch.substr(indexOfTemplate) : filePathMatch;
manifestFilePaths.push(directoryName + "/" + templatePath.trim());
});
}
return getManifestUrls(manifestFilePaths);
}
export function getChartDirectoryName(helmOutput: string): string {
// The output contains chart path in the following format - CHART PATH: C:\agent\_work\2\s\helm-chart-directory
let directoryName = "";
const chartPathMatch = helmOutput.match(matchPatternForChartPath);
if (chartPathMatch && chartPathMatch.length >= 1) {
let fullPath = chartPathMatch[1];
let indexOfLastSeparator = fullPath.lastIndexOf("\\");
directoryName = indexOfLastSeparator >= 0 ? fullPath.substr(indexOfLastSeparator + 1) : fullPath;
}
return directoryName;
}
export function getManifestUrls(manifestFilePaths: string[]): string[] {
let manifestUrls = [];
const branchName = getBranchName(branch);
for (const path of manifestFilePaths) {
let manifestUrl = "";
let normalisedPath = path.indexOf(workingDirectory) === 0 ? path.substr(workingDirectory.length) : path;
normalisedPath = normalisedPath.replace(/\\/g, "/");
if (repositoryProvider && (repositoryProvider.toLowerCase() === "githubenterprise" || repositoryProvider.toLowerCase() === "github")) {
if (normalisedPath.indexOf("/") != 0) {
// Prepend "/" if not present in path beginning as the path is appended as it is in manifest url to access github repo
normalisedPath = "/" + normalisedPath;
}
manifestUrl = repositoryUrl + "/blob/" + branchName + normalisedPath;
}
else if (repositoryProvider && repositoryProvider.toLowerCase() === "tfsgit") {
if (normalisedPath.indexOf("/") === 0) {
// Remove "/" from path if present in the beginning as we need to append path as a query string in manifest url to access tfs repo
normalisedPath = normalisedPath.substr(1);
}
manifestUrl = repositoryUrl + "?path=" + normalisedPath;
}
manifestUrls.push(manifestUrl);
}
return manifestUrls;
}
function getBranchName(ref: string): string {
const gitRefsHeadsPrefix = "refs/heads/";
if (ref && ref.indexOf(gitRefsHeadsPrefix) === 0) {
return ref.substr(gitRefsHeadsPrefix.length);
}
return ref;
}
function getPlatform(): string {
let platform: string = "Custom";
const connectionType = tl.getInput("connectionType");
if (connectionType === "Azure Resource Manager") {
platform = "AKS";
}
return platform;
}
export function IsJsonString(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
export function getPublishDeploymentRequestUrl(): string {
return orgUrl + tl.getVariable("System.TeamProject") + "/_apis/deployment/deploymentdetails?api-version=5.2-preview.1";
}
export function isDeploymentEntity(kind: string): boolean {
return deploymentTypes.some((type: string) => {
return kind.toLowerCase() === type;
});
}
export function isPodEntity(kind: string): boolean {
if (!kind) {
tl.warning("ResourceKindNotDefined");
return false;
}
return kind.toLowerCase() === "pod";
}
function getRelatedUrl(url: string, label: string): { [key: string]: string } | null {
if (url) {
return {
"url": url,
"label": label
};
}
else {
return null;
}
}
function getPipelineRunInfo(): any {
var deploymentPhaseIdentifier = tl.getVariable("Environment.DeploymentPhaseIdentifier")
if (deploymentPhaseIdentifier) {
return {
'planId': tl.getVariable("SYSTEM.PLANID"),
'deploymentPhaseIdentifier': tl.getVariable("Environment.DeploymentPhaseIdentifier"),
'jobName': tl.getVariable("SYSTEM.JOBNAME"),
'hubName': tl.getVariable("SYSTEM.HOSTTYPE")
};
}
return null;
}