@electrolux-oss/plugin-infrawallet-backend 0.1.3 → 0.1.4
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/config.d.ts +9 -3
- package/dist/index.cjs.js +214 -137
- package/dist/index.cjs.js.map +1 -1
- package/migrations/20240502114057_init.js +5 -20
- package/migrations/20240625084747_separate-table-for-category-mappings-overriding.js +44 -0
- package/package.json +1 -1
- package/seeds/default_category_mappings.js +471 -0
- package/seeds/20240618121807_default-gcp-category-mappings.js +0 -71
- package/seeds/init.js +0 -214
package/config.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
export interface Config {
|
|
2
2
|
backend: {
|
|
3
3
|
infraWallet: {
|
|
4
|
-
/**
|
|
5
|
-
* @deepVisibility secret
|
|
6
|
-
*/
|
|
7
4
|
integrations: {
|
|
8
5
|
azure?: [
|
|
9
6
|
{
|
|
@@ -11,6 +8,9 @@ export interface Config {
|
|
|
11
8
|
subscriptionId: string;
|
|
12
9
|
clientId: string;
|
|
13
10
|
tenantId: string;
|
|
11
|
+
/**
|
|
12
|
+
* @visibility secret
|
|
13
|
+
*/
|
|
14
14
|
clientSecret: string;
|
|
15
15
|
tags?: string[];
|
|
16
16
|
},
|
|
@@ -20,7 +20,13 @@ export interface Config {
|
|
|
20
20
|
name: string;
|
|
21
21
|
accountId: string;
|
|
22
22
|
assumedRoleName: string;
|
|
23
|
+
/**
|
|
24
|
+
* @visibility secret
|
|
25
|
+
*/
|
|
23
26
|
accessKeyId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* @visibility secret
|
|
29
|
+
*/
|
|
24
30
|
accessKeySecret?: string;
|
|
25
31
|
tags?: string[];
|
|
26
32
|
},
|
package/dist/index.cjs.js
CHANGED
|
@@ -11,8 +11,8 @@ var clientSts = require('@aws-sdk/client-sts');
|
|
|
11
11
|
var lodash = require('lodash');
|
|
12
12
|
var moment = require('moment');
|
|
13
13
|
var armCostmanagement = require('@azure/arm-costmanagement');
|
|
14
|
-
var identity = require('@azure/identity');
|
|
15
14
|
var coreRestPipeline = require('@azure/core-rest-pipeline');
|
|
15
|
+
var identity = require('@azure/identity');
|
|
16
16
|
var bigquery = require('@google-cloud/bigquery');
|
|
17
17
|
|
|
18
18
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
@@ -24,34 +24,76 @@ var moment__default = /*#__PURE__*/_interopDefaultCompat(moment);
|
|
|
24
24
|
async function getCategoryMappings(database, provider) {
|
|
25
25
|
const result = {};
|
|
26
26
|
const client = await database.getClient();
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const default_mappings = await client.where({ provider: provider.toLowerCase() }).select().from("category_mappings_default");
|
|
28
|
+
default_mappings.forEach((mapping) => {
|
|
29
|
+
if (typeof mapping.cloud_service_names === "string") {
|
|
30
|
+
JSON.parse(mapping.cloud_service_names).forEach((service) => {
|
|
31
|
+
result[service] = mapping.category;
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
mapping.cloud_service_names.forEach((service) => {
|
|
35
|
+
result[service] = mapping.category;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const override_mappings = await client.where({ provider }).select().from("category_mappings_override");
|
|
40
|
+
override_mappings.forEach((mapping) => {
|
|
29
41
|
if (typeof mapping.cloud_service_names === "string") {
|
|
30
|
-
|
|
42
|
+
JSON.parse(mapping.cloud_service_names).forEach((service) => {
|
|
43
|
+
result[service] = mapping.category;
|
|
44
|
+
});
|
|
31
45
|
} else {
|
|
32
|
-
|
|
46
|
+
mapping.cloud_service_names.forEach((service) => {
|
|
47
|
+
result[service] = mapping.category;
|
|
48
|
+
});
|
|
33
49
|
}
|
|
34
50
|
});
|
|
35
51
|
return result;
|
|
36
52
|
}
|
|
37
53
|
function getCategoryByServiceName(serviceName, categoryMappings) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (serviceNames && serviceNames.includes(serviceName)) {
|
|
41
|
-
return key;
|
|
42
|
-
}
|
|
54
|
+
if (serviceName in categoryMappings) {
|
|
55
|
+
return categoryMappings[serviceName];
|
|
43
56
|
}
|
|
44
57
|
return "Uncategorized";
|
|
45
58
|
}
|
|
59
|
+
async function getReportsFromCache(cache, provider, configKey, query) {
|
|
60
|
+
const cacheKey = [
|
|
61
|
+
provider,
|
|
62
|
+
configKey,
|
|
63
|
+
query.filters,
|
|
64
|
+
query.groups,
|
|
65
|
+
query.granularity,
|
|
66
|
+
query.startTime,
|
|
67
|
+
query.endTime
|
|
68
|
+
].join("_");
|
|
69
|
+
const cachedCosts = await cache.get(cacheKey);
|
|
70
|
+
return cachedCosts;
|
|
71
|
+
}
|
|
72
|
+
async function setReportsToCache(cache, reports, provider, configKey, query, ttl) {
|
|
73
|
+
const cacheKey = [
|
|
74
|
+
provider,
|
|
75
|
+
configKey,
|
|
76
|
+
query.filters,
|
|
77
|
+
query.groups,
|
|
78
|
+
query.granularity,
|
|
79
|
+
query.startTime,
|
|
80
|
+
query.endTime
|
|
81
|
+
].join("_");
|
|
82
|
+
await cache.set(cacheKey, reports, {
|
|
83
|
+
ttl: ttl
|
|
84
|
+
});
|
|
85
|
+
}
|
|
46
86
|
|
|
47
87
|
class AwsClient {
|
|
48
|
-
constructor(config, database, logger) {
|
|
88
|
+
constructor(providerName, config, database, cache, logger) {
|
|
89
|
+
this.providerName = providerName;
|
|
49
90
|
this.config = config;
|
|
50
91
|
this.database = database;
|
|
92
|
+
this.cache = cache;
|
|
51
93
|
this.logger = logger;
|
|
52
94
|
}
|
|
53
|
-
static create(config, database, logger) {
|
|
54
|
-
return new AwsClient(config, database, logger);
|
|
95
|
+
static create(config, database, cache, logger) {
|
|
96
|
+
return new AwsClient("AWS", config, database, cache, logger);
|
|
55
97
|
}
|
|
56
98
|
convertServiceName(serviceName) {
|
|
57
99
|
let convertedName = serviceName;
|
|
@@ -61,16 +103,13 @@ class AwsClient {
|
|
|
61
103
|
["Virtual Private Cloud", "VPC (Virtual Private Cloud)"],
|
|
62
104
|
["Relational Database Service", "RDS (Relational Database Service)"],
|
|
63
105
|
["Simple Storage Service", "S3 (Simple Storage Service)"],
|
|
64
|
-
[
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
],
|
|
68
|
-
[
|
|
69
|
-
"Elastic Container Service for Kubernetes",
|
|
70
|
-
"EKS (Elastic Container Service for Kubernetes)"
|
|
71
|
-
],
|
|
106
|
+
["Managed Streaming for Apache Kafka", "MSK (Managed Streaming for Apache Kafka)"],
|
|
107
|
+
["Elastic Container Service for Kubernetes", "EKS (Elastic Container Service for Kubernetes)"],
|
|
108
|
+
["Elastic Container Service", "ECS (Elastic Container Service)"],
|
|
109
|
+
["EC2 Container Registry (ECR)", "ECR (Elastic Container Registry)"],
|
|
72
110
|
["Simple Queue Service", "SQS (Simple Queue Service)"],
|
|
73
|
-
["Simple Notification Service", "SNS (Simple Notification Service)"]
|
|
111
|
+
["Simple Notification Service", "SNS (Simple Notification Service)"],
|
|
112
|
+
["Database Migration Service", "DMS (Database Migration Service)"]
|
|
74
113
|
]);
|
|
75
114
|
for (const prefix of prefixes) {
|
|
76
115
|
if (serviceName.startsWith(prefix)) {
|
|
@@ -80,24 +119,31 @@ class AwsClient {
|
|
|
80
119
|
if (aliases.has(convertedName)) {
|
|
81
120
|
convertedName = aliases.get(convertedName) || convertedName;
|
|
82
121
|
}
|
|
83
|
-
return
|
|
122
|
+
return `${this.providerName}/${convertedName}`;
|
|
84
123
|
}
|
|
85
124
|
async fetchCostsFromCloud(query) {
|
|
86
|
-
const conf = this.config.getOptionalConfigArray(
|
|
87
|
-
"backend.infraWallet.integrations.aws"
|
|
88
|
-
);
|
|
125
|
+
const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.aws");
|
|
89
126
|
if (!conf) {
|
|
90
|
-
return [];
|
|
127
|
+
return { reports: [], errors: [] };
|
|
91
128
|
}
|
|
92
129
|
const promises = [];
|
|
93
130
|
const results = [];
|
|
131
|
+
const errors = [];
|
|
94
132
|
query.groups.split(",").forEach((group) => {
|
|
95
133
|
if (group.includes(":")) {
|
|
96
134
|
group.split(":");
|
|
97
135
|
}
|
|
98
136
|
});
|
|
99
137
|
for (const c of conf) {
|
|
100
|
-
const
|
|
138
|
+
const accountName = c.getString("name");
|
|
139
|
+
const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
|
|
140
|
+
if (cachedCosts) {
|
|
141
|
+
this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
|
|
142
|
+
cachedCosts.map((cost) => {
|
|
143
|
+
results.push(cost);
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
101
147
|
const accountId = c.getString("accountId");
|
|
102
148
|
const assumedRoleName = c.getString("assumedRoleName");
|
|
103
149
|
const accessKeyId = c.getOptionalString("accessKeyId");
|
|
@@ -108,7 +154,7 @@ class AwsClient {
|
|
|
108
154
|
const [k, v] = tag.split(":");
|
|
109
155
|
tagKeyValues[k.trim()] = v.trim();
|
|
110
156
|
});
|
|
111
|
-
const categoryMappings = await getCategoryMappings(this.database,
|
|
157
|
+
const categoryMappings = await getCategoryMappings(this.database, this.providerName);
|
|
112
158
|
let stsParams = {};
|
|
113
159
|
if (accessKeyId && accessKeySecret) {
|
|
114
160
|
stsParams = {
|
|
@@ -178,26 +224,21 @@ class AwsClient {
|
|
|
178
224
|
row.Groups.forEach((group) => {
|
|
179
225
|
var _a3;
|
|
180
226
|
const serviceName = group.Keys ? group.Keys[0] : "";
|
|
181
|
-
const keyName = `${
|
|
227
|
+
const keyName = `${accountName}_${serviceName}`;
|
|
182
228
|
if (!accumulator[keyName]) {
|
|
183
229
|
accumulator[keyName] = {
|
|
184
230
|
id: keyName,
|
|
185
|
-
name:
|
|
231
|
+
name: `${this.providerName}/${accountName}`,
|
|
186
232
|
service: this.convertServiceName(serviceName),
|
|
187
|
-
category: getCategoryByServiceName(
|
|
188
|
-
|
|
189
|
-
categoryMappings
|
|
190
|
-
),
|
|
191
|
-
provider: "AWS",
|
|
233
|
+
category: getCategoryByServiceName(serviceName, categoryMappings),
|
|
234
|
+
provider: this.providerName,
|
|
192
235
|
reports: {},
|
|
193
236
|
...tagKeyValues
|
|
194
237
|
};
|
|
195
238
|
}
|
|
196
239
|
const groupMetrics = group.Metrics;
|
|
197
240
|
if (groupMetrics !== void 0) {
|
|
198
|
-
accumulator[keyName].reports[period] = parseFloat(
|
|
199
|
-
(_a3 = groupMetrics.UnblendedCost.Amount) != null ? _a3 : "0.0"
|
|
200
|
-
);
|
|
241
|
+
accumulator[keyName].reports[period] = parseFloat((_a3 = groupMetrics.UnblendedCost.Amount) != null ? _a3 : "0.0");
|
|
201
242
|
}
|
|
202
243
|
});
|
|
203
244
|
}
|
|
@@ -205,28 +246,46 @@ class AwsClient {
|
|
|
205
246
|
},
|
|
206
247
|
{}
|
|
207
248
|
);
|
|
249
|
+
await setReportsToCache(
|
|
250
|
+
this.cache,
|
|
251
|
+
Object.values(transformedData),
|
|
252
|
+
this.providerName,
|
|
253
|
+
accountName,
|
|
254
|
+
query,
|
|
255
|
+
60 * 60 * 2 * 1e3
|
|
256
|
+
);
|
|
208
257
|
Object.values(transformedData).map((value) => {
|
|
209
258
|
results.push(value);
|
|
210
259
|
});
|
|
211
260
|
} catch (e) {
|
|
212
261
|
this.logger.error(e);
|
|
262
|
+
errors.push({
|
|
263
|
+
provider: this.providerName,
|
|
264
|
+
name: `${this.providerName}/${accountName}`,
|
|
265
|
+
error: e.message
|
|
266
|
+
});
|
|
213
267
|
}
|
|
214
268
|
})();
|
|
215
269
|
promises.push(promise);
|
|
216
270
|
}
|
|
217
271
|
await Promise.all(promises);
|
|
218
|
-
return
|
|
272
|
+
return {
|
|
273
|
+
reports: results,
|
|
274
|
+
errors
|
|
275
|
+
};
|
|
219
276
|
}
|
|
220
277
|
}
|
|
221
278
|
|
|
222
279
|
class AzureClient {
|
|
223
|
-
constructor(config, database, logger) {
|
|
280
|
+
constructor(providerName, config, database, cache, logger) {
|
|
281
|
+
this.providerName = providerName;
|
|
224
282
|
this.config = config;
|
|
225
283
|
this.database = database;
|
|
284
|
+
this.cache = cache;
|
|
226
285
|
this.logger = logger;
|
|
227
286
|
}
|
|
228
|
-
static create(config, database, logger) {
|
|
229
|
-
return new AzureClient(config, database, logger);
|
|
287
|
+
static create(config, database, cache, logger) {
|
|
288
|
+
return new AzureClient("Azure", config, database, cache, logger);
|
|
230
289
|
}
|
|
231
290
|
convertServiceName(serviceName) {
|
|
232
291
|
let convertedName = serviceName;
|
|
@@ -236,7 +295,7 @@ class AzureClient {
|
|
|
236
295
|
convertedName = serviceName.slice(prefix.length).trim();
|
|
237
296
|
}
|
|
238
297
|
}
|
|
239
|
-
return
|
|
298
|
+
return `${this.providerName}/${convertedName}`;
|
|
240
299
|
}
|
|
241
300
|
formatDate(dateNumber) {
|
|
242
301
|
const dateString = dateNumber.toString();
|
|
@@ -263,7 +322,10 @@ class AzureClient {
|
|
|
263
322
|
if (response.status === 200) {
|
|
264
323
|
return JSON.parse(response.bodyAsText || "{}");
|
|
265
324
|
} else if (response.status === 429) {
|
|
266
|
-
const retryAfter = parseInt(
|
|
325
|
+
const retryAfter = parseInt(
|
|
326
|
+
response.headers.get("x-ms-ratelimit-microsoft.costmanagement-entity-retry-after") || "60",
|
|
327
|
+
10
|
|
328
|
+
);
|
|
267
329
|
this.logger.warn(`Hit Azure rate limit, retrying after ${retryAfter} seconds...`);
|
|
268
330
|
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3));
|
|
269
331
|
retries++;
|
|
@@ -297,30 +359,30 @@ class AzureClient {
|
|
|
297
359
|
return allResults;
|
|
298
360
|
}
|
|
299
361
|
async fetchCostsFromCloud(query) {
|
|
300
|
-
const conf = this.config.getOptionalConfigArray(
|
|
301
|
-
"backend.infraWallet.integrations.azure"
|
|
302
|
-
);
|
|
362
|
+
const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.azure");
|
|
303
363
|
if (!conf) {
|
|
304
|
-
return [];
|
|
364
|
+
return { reports: [], errors: [] };
|
|
305
365
|
}
|
|
306
|
-
const categoryMappings = await getCategoryMappings(
|
|
307
|
-
this.database,
|
|
308
|
-
"azure"
|
|
309
|
-
);
|
|
366
|
+
const categoryMappings = await getCategoryMappings(this.database, this.providerName);
|
|
310
367
|
const promises = [];
|
|
311
368
|
const results = [];
|
|
369
|
+
const errors = [];
|
|
312
370
|
const groupPairs = [{ type: "Dimension", name: "ServiceName" }];
|
|
313
371
|
for (const c of conf) {
|
|
314
|
-
const
|
|
372
|
+
const accountName = c.getString("name");
|
|
373
|
+
const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
|
|
374
|
+
if (cachedCosts) {
|
|
375
|
+
this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
|
|
376
|
+
cachedCosts.map((cost) => {
|
|
377
|
+
results.push(cost);
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
315
381
|
const subscriptionId = c.getString("subscriptionId");
|
|
316
382
|
const tenantId = c.getString("tenantId");
|
|
317
383
|
const clientId = c.getString("clientId");
|
|
318
384
|
const clientSecret = c.getString("clientSecret");
|
|
319
|
-
const credential = new identity.ClientSecretCredential(
|
|
320
|
-
tenantId,
|
|
321
|
-
clientId,
|
|
322
|
-
clientSecret
|
|
323
|
-
);
|
|
385
|
+
const credential = new identity.ClientSecretCredential(tenantId, clientId, clientSecret);
|
|
324
386
|
const client = new armCostmanagement.CostManagementClient(credential);
|
|
325
387
|
const tags = c.getOptionalStringArray("tags");
|
|
326
388
|
const tagKeyValues = {};
|
|
@@ -347,17 +409,17 @@ class AzureClient {
|
|
|
347
409
|
if (query.granularity.toUpperCase() === "DAILY") {
|
|
348
410
|
date = this.formatDate(date);
|
|
349
411
|
}
|
|
350
|
-
let keyName =
|
|
412
|
+
let keyName = accountName;
|
|
351
413
|
for (let i = 0; i < groupPairs.length; i++) {
|
|
352
414
|
keyName += `->${row[i + 2]}`;
|
|
353
415
|
}
|
|
354
416
|
if (!accumulator[keyName]) {
|
|
355
417
|
accumulator[keyName] = {
|
|
356
418
|
id: keyName,
|
|
357
|
-
name:
|
|
419
|
+
name: `${this.providerName}/${accountName}`,
|
|
358
420
|
service: this.convertServiceName(serviceName),
|
|
359
421
|
category: getCategoryByServiceName(serviceName, categoryMappings),
|
|
360
|
-
provider:
|
|
422
|
+
provider: this.providerName,
|
|
361
423
|
reports: {},
|
|
362
424
|
...tagKeyValues
|
|
363
425
|
};
|
|
@@ -374,38 +436,56 @@ class AzureClient {
|
|
|
374
436
|
},
|
|
375
437
|
{}
|
|
376
438
|
);
|
|
439
|
+
await setReportsToCache(
|
|
440
|
+
this.cache,
|
|
441
|
+
Object.values(transformedData),
|
|
442
|
+
this.providerName,
|
|
443
|
+
accountName,
|
|
444
|
+
query,
|
|
445
|
+
60 * 60 * 2 * 1e3
|
|
446
|
+
);
|
|
377
447
|
Object.values(transformedData).map((value) => {
|
|
378
448
|
results.push(value);
|
|
379
449
|
});
|
|
380
450
|
} catch (e) {
|
|
381
451
|
this.logger.error(e);
|
|
452
|
+
errors.push({
|
|
453
|
+
provider: this.providerName,
|
|
454
|
+
name: `${this.providerName}/${accountName}`,
|
|
455
|
+
error: e.message
|
|
456
|
+
});
|
|
382
457
|
}
|
|
383
458
|
})();
|
|
384
459
|
promises.push(promise);
|
|
385
460
|
}
|
|
386
461
|
await Promise.all(promises);
|
|
387
|
-
return
|
|
462
|
+
return {
|
|
463
|
+
reports: results,
|
|
464
|
+
errors
|
|
465
|
+
};
|
|
388
466
|
}
|
|
389
467
|
}
|
|
390
468
|
|
|
391
469
|
class GCPClient {
|
|
392
|
-
constructor(config, database, logger) {
|
|
470
|
+
constructor(providerName, config, database, cache, logger) {
|
|
471
|
+
this.providerName = providerName;
|
|
393
472
|
this.config = config;
|
|
394
473
|
this.database = database;
|
|
474
|
+
this.cache = cache;
|
|
395
475
|
this.logger = logger;
|
|
396
476
|
}
|
|
397
|
-
static create(config, database, logger) {
|
|
398
|
-
return new GCPClient(config, database, logger);
|
|
477
|
+
static create(config, database, cache, logger) {
|
|
478
|
+
return new GCPClient("GCP", config, database, cache, logger);
|
|
399
479
|
}
|
|
400
480
|
convertServiceName(serviceName) {
|
|
401
481
|
let convertedName = serviceName;
|
|
402
|
-
const prefixes = ["
|
|
482
|
+
const prefixes = ["Google Cloud"];
|
|
403
483
|
for (const prefix of prefixes) {
|
|
404
484
|
if (serviceName.startsWith(prefix)) {
|
|
405
485
|
convertedName = serviceName.slice(prefix.length).trim();
|
|
406
486
|
}
|
|
407
487
|
}
|
|
408
|
-
return
|
|
488
|
+
return `${this.providerName}/${convertedName}`;
|
|
409
489
|
}
|
|
410
490
|
async queryBigQuery(keyFilePath, projectId, datasetId, tableId, query) {
|
|
411
491
|
const options = {
|
|
@@ -443,16 +523,23 @@ class GCPClient {
|
|
|
443
523
|
}
|
|
444
524
|
}
|
|
445
525
|
async fetchCostsFromCloud(query) {
|
|
446
|
-
const conf = this.config.getOptionalConfigArray(
|
|
447
|
-
"backend.infraWallet.integrations.gcp"
|
|
448
|
-
);
|
|
526
|
+
const conf = this.config.getOptionalConfigArray("backend.infraWallet.integrations.gcp");
|
|
449
527
|
if (!conf) {
|
|
450
|
-
return [];
|
|
528
|
+
return { reports: [], errors: [] };
|
|
451
529
|
}
|
|
452
530
|
const promises = [];
|
|
453
531
|
const results = [];
|
|
532
|
+
const errors = [];
|
|
454
533
|
for (const c of conf) {
|
|
455
|
-
const
|
|
534
|
+
const accountName = c.getString("name");
|
|
535
|
+
const cachedCosts = await getReportsFromCache(this.cache, this.providerName, accountName, query);
|
|
536
|
+
if (cachedCosts) {
|
|
537
|
+
this.logger.debug(`${this.providerName}/${accountName} costs from cache`);
|
|
538
|
+
cachedCosts.map((cost) => {
|
|
539
|
+
results.push(cost);
|
|
540
|
+
});
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
456
543
|
const keyFilePath = c.getString("keyFilePath");
|
|
457
544
|
const projectId = c.getString("projectId");
|
|
458
545
|
const datasetId = c.getString("datasetId");
|
|
@@ -463,31 +550,22 @@ class GCPClient {
|
|
|
463
550
|
const [k, v] = tag.split(":");
|
|
464
551
|
tagKeyValues[k.trim()] = v.trim();
|
|
465
552
|
});
|
|
466
|
-
const categoryMappings = await getCategoryMappings(this.database,
|
|
553
|
+
const categoryMappings = await getCategoryMappings(this.database, this.providerName);
|
|
467
554
|
const promise = (async () => {
|
|
468
555
|
try {
|
|
469
|
-
const costResponse = await this.queryBigQuery(
|
|
470
|
-
keyFilePath,
|
|
471
|
-
projectId,
|
|
472
|
-
datasetId,
|
|
473
|
-
tableId,
|
|
474
|
-
query
|
|
475
|
-
);
|
|
556
|
+
const costResponse = await this.queryBigQuery(keyFilePath, projectId, datasetId, tableId, query);
|
|
476
557
|
const transformedData = lodash.reduce(
|
|
477
558
|
costResponse,
|
|
478
559
|
(acc, row) => {
|
|
479
560
|
const period = row.period;
|
|
480
|
-
const keyName = `${
|
|
561
|
+
const keyName = `${accountName}_${row.project}_${row.service}`;
|
|
481
562
|
if (!acc[keyName]) {
|
|
482
563
|
acc[keyName] = {
|
|
483
564
|
id: keyName,
|
|
484
|
-
name:
|
|
565
|
+
name: `${this.providerName}/${accountName}`,
|
|
485
566
|
service: this.convertServiceName(row.service),
|
|
486
|
-
category: getCategoryByServiceName(
|
|
487
|
-
|
|
488
|
-
categoryMappings
|
|
489
|
-
),
|
|
490
|
-
provider: "GCP",
|
|
567
|
+
category: getCategoryByServiceName(row.service, categoryMappings),
|
|
568
|
+
provider: this.providerName,
|
|
491
569
|
reports: {},
|
|
492
570
|
...{ project: row.project },
|
|
493
571
|
// TODO: how should we handle the project field? for now, we add project name as a field in the report
|
|
@@ -500,51 +578,56 @@ class GCPClient {
|
|
|
500
578
|
},
|
|
501
579
|
{}
|
|
502
580
|
);
|
|
581
|
+
await setReportsToCache(
|
|
582
|
+
this.cache,
|
|
583
|
+
Object.values(transformedData),
|
|
584
|
+
this.providerName,
|
|
585
|
+
accountName,
|
|
586
|
+
query,
|
|
587
|
+
60 * 60 * 2 * 1e3
|
|
588
|
+
);
|
|
503
589
|
Object.values(transformedData).map((value) => {
|
|
504
590
|
results.push(value);
|
|
505
591
|
});
|
|
506
592
|
} catch (e) {
|
|
507
593
|
this.logger.error(e);
|
|
594
|
+
errors.push({
|
|
595
|
+
provider: this.providerName,
|
|
596
|
+
name: `${this.providerName}/${accountName}`,
|
|
597
|
+
error: e.message
|
|
598
|
+
});
|
|
508
599
|
}
|
|
509
600
|
})();
|
|
510
601
|
promises.push(promise);
|
|
511
602
|
}
|
|
512
603
|
await Promise.all(promises);
|
|
513
|
-
return
|
|
604
|
+
return {
|
|
605
|
+
reports: results,
|
|
606
|
+
errors
|
|
607
|
+
};
|
|
514
608
|
}
|
|
515
609
|
}
|
|
516
610
|
|
|
517
611
|
async function setUpDatabase(database) {
|
|
518
612
|
var _a;
|
|
519
613
|
const client = await database.getClient();
|
|
520
|
-
const migrationsDir = backendPluginApi.resolvePackagePath(
|
|
521
|
-
"@electrolux-oss/plugin-infrawallet-backend",
|
|
522
|
-
"migrations"
|
|
523
|
-
);
|
|
614
|
+
const migrationsDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "migrations");
|
|
524
615
|
if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
|
|
525
616
|
await client.migrate.latest({
|
|
526
617
|
directory: migrationsDir
|
|
527
618
|
});
|
|
528
619
|
}
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
);
|
|
532
|
-
if (category_mappings_count[0].c === 0 || category_mappings_count[0].c === "0") {
|
|
533
|
-
const seedsDir = backendPluginApi.resolvePackagePath(
|
|
534
|
-
"@electrolux-oss/plugin-infrawallet-backend",
|
|
535
|
-
"seeds"
|
|
536
|
-
);
|
|
537
|
-
await client.seed.run({ directory: seedsDir });
|
|
538
|
-
}
|
|
620
|
+
const seedsDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "seeds");
|
|
621
|
+
await client.seed.run({ directory: seedsDir });
|
|
539
622
|
}
|
|
540
623
|
async function createRouter(options) {
|
|
541
624
|
const { logger, config, cache, database } = options;
|
|
542
625
|
await setUpDatabase(database);
|
|
543
626
|
const router = Router__default.default();
|
|
544
627
|
router.use(express__default.default.json());
|
|
545
|
-
const azureClient = AzureClient.create(config, database, logger);
|
|
546
|
-
const awsClient = AwsClient.create(config, database, logger);
|
|
547
|
-
const gcpClient = GCPClient.create(config, database, logger);
|
|
628
|
+
const azureClient = AzureClient.create(config, database, cache, logger);
|
|
629
|
+
const awsClient = AwsClient.create(config, database, cache, logger);
|
|
630
|
+
const gcpClient = GCPClient.create(config, database, cache, logger);
|
|
548
631
|
const cloudClients = [azureClient, awsClient, gcpClient];
|
|
549
632
|
router.get("/health", (_, response) => {
|
|
550
633
|
logger.info("PONG!");
|
|
@@ -558,46 +641,40 @@ async function createRouter(options) {
|
|
|
558
641
|
const endTime = request.query.endTime;
|
|
559
642
|
const promises = [];
|
|
560
643
|
const results = [];
|
|
644
|
+
const errors = [];
|
|
561
645
|
cloudClients.forEach(async (client) => {
|
|
562
646
|
const fetchCloudCosts = (async () => {
|
|
563
|
-
|
|
564
|
-
client.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
647
|
+
try {
|
|
648
|
+
const clientResponse = await client.fetchCostsFromCloud({
|
|
649
|
+
filters,
|
|
650
|
+
groups,
|
|
651
|
+
granularity,
|
|
652
|
+
startTime,
|
|
653
|
+
endTime
|
|
654
|
+
});
|
|
655
|
+
clientResponse.errors.forEach((e) => {
|
|
656
|
+
errors.push(e);
|
|
657
|
+
});
|
|
658
|
+
clientResponse.reports.forEach((cost) => {
|
|
575
659
|
results.push(cost);
|
|
576
660
|
});
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
endTime
|
|
585
|
-
});
|
|
586
|
-
await cache.set(cacheKey, costs, {
|
|
587
|
-
ttl: 60 * 60 * 2 * 1e3
|
|
588
|
-
});
|
|
589
|
-
costs.forEach((cost) => {
|
|
590
|
-
results.push(cost);
|
|
591
|
-
});
|
|
592
|
-
} catch (e) {
|
|
593
|
-
logger.error(e);
|
|
594
|
-
}
|
|
661
|
+
} catch (e) {
|
|
662
|
+
logger.error(e);
|
|
663
|
+
errors.push({
|
|
664
|
+
provider: client.constructor.name,
|
|
665
|
+
name: client.constructor.name,
|
|
666
|
+
error: e.message
|
|
667
|
+
});
|
|
595
668
|
}
|
|
596
669
|
})();
|
|
597
670
|
promises.push(fetchCloudCosts);
|
|
598
671
|
});
|
|
599
672
|
await Promise.all(promises);
|
|
600
|
-
|
|
673
|
+
if (errors.length > 0) {
|
|
674
|
+
response.status(207).json({ data: results, errors, status: 207 });
|
|
675
|
+
} else {
|
|
676
|
+
response.json({ data: results, errors, status: 200 });
|
|
677
|
+
}
|
|
601
678
|
});
|
|
602
679
|
router.use(backendCommon.errorHandler());
|
|
603
680
|
return router;
|