@electrolux-oss/plugin-infrawallet-backend 0.1.0

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/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # infrawallet
2
+
3
+ Welcome to the infrawallet backend plugin!
4
+
5
+ _This plugin was created through the Backstage CLI_
6
+
7
+ ## Getting started
8
+
9
+ Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
10
+ start` in the root directory, and then navigating to [/infraWalletPlugin/health](http://localhost:7007/api/infraWalletPlugin/health).
11
+
12
+ You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
13
+ This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
14
+ It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
package/config.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export interface Config {
2
+ backend: {
3
+ infraWallet: {
4
+ /**
5
+ * @deepVisibility secret
6
+ */
7
+ integrations: {
8
+ azure?: [
9
+ {
10
+ name: string;
11
+ subscriptionId: string;
12
+ clientId: string;
13
+ tenantId: string;
14
+ clientSecret: string;
15
+ tags?: string[];
16
+ },
17
+ ];
18
+ aws?: [
19
+ {
20
+ name: string;
21
+ accountId: string;
22
+ assumedRoleName: string;
23
+ accessKeyId: string;
24
+ accessKeySecret: string;
25
+ tags?: string[];
26
+ },
27
+ ];
28
+ };
29
+ };
30
+ };
31
+ }
@@ -0,0 +1,376 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var backendCommon = require('@backstage/backend-common');
6
+ var express = require('express');
7
+ var Router = require('express-promise-router');
8
+ var clientCostExplorer = require('@aws-sdk/client-cost-explorer');
9
+ var clientSts = require('@aws-sdk/client-sts');
10
+ var lodash = require('lodash');
11
+ var moment = require('moment');
12
+ var armCostmanagement = require('@azure/arm-costmanagement');
13
+ var identity = require('@azure/identity');
14
+ var backendPluginApi = require('@backstage/backend-plugin-api');
15
+
16
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
17
+
18
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
19
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
20
+ var moment__default = /*#__PURE__*/_interopDefaultCompat(moment);
21
+
22
+ async function getCategoryMappings(database, provider) {
23
+ const result = {};
24
+ const client = await database.getClient();
25
+ const mappings = await client.where({ provider }).select().from("category_mappings");
26
+ mappings.forEach((mapping) => {
27
+ if (typeof mapping.cloud_service_names === "string") {
28
+ result[mapping.category] = JSON.parse(mapping.cloud_service_names);
29
+ } else {
30
+ result[mapping.category] = mapping.cloud_service_names;
31
+ }
32
+ });
33
+ return result;
34
+ }
35
+ function getCategoryByServiceName(serviceName, categoryMappings) {
36
+ for (const key of Object.keys(categoryMappings)) {
37
+ const serviceNames = categoryMappings[key];
38
+ if (serviceNames && serviceNames.includes(serviceName)) {
39
+ return key;
40
+ }
41
+ }
42
+ return "Uncategorized";
43
+ }
44
+
45
+ class AwsClient {
46
+ constructor(config, database) {
47
+ this.config = config;
48
+ this.database = database;
49
+ }
50
+ static create(config, database) {
51
+ return new AwsClient(config, database);
52
+ }
53
+ async fetchCostsFromCloud(query) {
54
+ const conf = this.config.getOptionalConfigArray(
55
+ "backend.infraWallet.integrations.aws"
56
+ );
57
+ if (!conf) {
58
+ return [];
59
+ }
60
+ const promises = [];
61
+ const results = [];
62
+ query.groups.split(",").forEach((group) => {
63
+ if (group.includes(":")) {
64
+ group.split(":");
65
+ }
66
+ });
67
+ for (const c of conf) {
68
+ const name = c.getOptionalString("name");
69
+ const accountId = c.getOptionalString("accountId");
70
+ const assumedRoleName = c.getOptionalString("assumedRoleName");
71
+ const accessKeyId = c.getOptionalString("accessKeyId");
72
+ const accessKeySecret = c.getOptionalString("accessKeySecret");
73
+ const tags = c.getOptionalStringArray("tags");
74
+ const tagKeyValues = {};
75
+ tags == null ? void 0 : tags.forEach((tag) => {
76
+ const [k, v] = tag.split(":");
77
+ tagKeyValues[k.trim()] = v.trim();
78
+ });
79
+ const categoryMappings = await getCategoryMappings(this.database, "aws");
80
+ const promise = (async () => {
81
+ var _a, _b, _c;
82
+ const client = new clientSts.STSClient({
83
+ region: "us-east-1",
84
+ credentials: {
85
+ accessKeyId,
86
+ secretAccessKey: accessKeySecret
87
+ }
88
+ });
89
+ const commandInput = {
90
+ // AssumeRoleRequest
91
+ RoleArn: `arn:aws:iam::${accountId}:role/${assumedRoleName}`,
92
+ RoleSessionName: "AssumeRoleSession1"
93
+ };
94
+ const assumeRoleCommand = new clientSts.AssumeRoleCommand(commandInput);
95
+ const assumeRoleResponse = await client.send(assumeRoleCommand);
96
+ const awsCeClient = new clientCostExplorer.CostExplorerClient({
97
+ region: "us-east-1",
98
+ credentials: {
99
+ accessKeyId: (_a = assumeRoleResponse.Credentials) == null ? void 0 : _a.AccessKeyId,
100
+ secretAccessKey: (_b = assumeRoleResponse.Credentials) == null ? void 0 : _b.SecretAccessKey,
101
+ sessionToken: (_c = assumeRoleResponse.Credentials) == null ? void 0 : _c.SessionToken
102
+ }
103
+ });
104
+ const input = {
105
+ TimePeriod: {
106
+ Start: moment__default.default(parseInt(query.startTime, 10)).format("YYYY-MM-DD"),
107
+ End: moment__default.default(parseInt(query.endTime, 10)).format("YYYY-MM-DD")
108
+ },
109
+ Granularity: query.granularity.toUpperCase(),
110
+ Filter: { Dimensions: { Key: "RECORD_TYPE", Values: ["Usage"] } },
111
+ GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }],
112
+ Metrics: ["UnblendedCost"]
113
+ };
114
+ const getCostCommand = new clientCostExplorer.GetCostAndUsageCommand(input);
115
+ const costAndusageResponse = await awsCeClient.send(getCostCommand);
116
+ const transformedData = lodash.reduce(
117
+ costAndusageResponse.ResultsByTime,
118
+ (acc, row) => {
119
+ const period = row.TimePeriod.Start.substring(0, 7);
120
+ row.Groups.forEach((group) => {
121
+ const keyName = `${name}_${group.Keys[0]}`;
122
+ if (!acc[keyName]) {
123
+ acc[keyName] = {
124
+ id: keyName,
125
+ name,
126
+ service: `${group.Keys[0]} (AWS)`,
127
+ category: getCategoryByServiceName(
128
+ group.Keys[0],
129
+ categoryMappings
130
+ ),
131
+ provider: "AWS",
132
+ reports: {},
133
+ ...tagKeyValues
134
+ };
135
+ }
136
+ acc[keyName].reports[period] = parseFloat(
137
+ group.Metrics.UnblendedCost.Amount
138
+ );
139
+ });
140
+ return acc;
141
+ },
142
+ {}
143
+ );
144
+ Object.values(transformedData).map((value) => {
145
+ results.push(value);
146
+ });
147
+ })();
148
+ promises.push(promise);
149
+ }
150
+ await Promise.all(promises);
151
+ return results;
152
+ }
153
+ }
154
+
155
+ class AzureClient {
156
+ constructor(config, database) {
157
+ this.config = config;
158
+ this.database = database;
159
+ }
160
+ static create(config, database) {
161
+ return new AzureClient(config, database);
162
+ }
163
+ async queryAzureCostExplorer(azureClient, subscription, granularity, groups, startDate, endDate) {
164
+ const scope = `/subscriptions/${subscription}`;
165
+ const query = {
166
+ type: "ActualCost",
167
+ dataset: {
168
+ granularity,
169
+ aggregation: { totalCostUSD: { name: "CostUSD", function: "Sum" } },
170
+ grouping: groups
171
+ },
172
+ timeframe: "Custom",
173
+ timePeriod: {
174
+ from: startDate.toISOString(),
175
+ to: endDate.toISOString()
176
+ }
177
+ };
178
+ const result = await azureClient.query.usage(scope, query);
179
+ return result;
180
+ }
181
+ async fetchCostsFromCloud(query) {
182
+ const conf = this.config.getOptionalConfigArray(
183
+ "backend.infraWallet.integrations.azure"
184
+ );
185
+ if (!conf) {
186
+ return [];
187
+ }
188
+ const promises = [];
189
+ const results = [];
190
+ const groupPairs = [{ type: "Dimension", name: "ServiceName" }];
191
+ for (const c of conf) {
192
+ const name = c.getOptionalString("name");
193
+ const subscriptionId = c.getOptionalString("subscriptionId");
194
+ const tenantId = c.getOptionalString("tenantId");
195
+ const clientId = c.getOptionalString("clientId");
196
+ const clientSecret = c.getOptionalString("clientSecret");
197
+ const credential = new identity.ClientSecretCredential(
198
+ tenantId,
199
+ clientId,
200
+ clientSecret
201
+ );
202
+ const client = new armCostmanagement.CostManagementClient(credential);
203
+ const tags = c.getOptionalStringArray("tags");
204
+ const tagKeyValues = {};
205
+ tags == null ? void 0 : tags.forEach((tag) => {
206
+ const [k, v] = tag.split(":");
207
+ tagKeyValues[k.trim()] = v.trim();
208
+ });
209
+ const categoryMappings = await getCategoryMappings(
210
+ this.database,
211
+ "azure"
212
+ );
213
+ const promise = (async () => {
214
+ try {
215
+ const costResponse = await this.queryAzureCostExplorer(
216
+ client,
217
+ subscriptionId,
218
+ query.granularity,
219
+ groupPairs,
220
+ moment__default.default(parseInt(query.startTime, 10)),
221
+ moment__default.default(parseInt(query.endTime, 10))
222
+ );
223
+ const transformedData = lodash.reduce(
224
+ costResponse.rows,
225
+ (acc, row) => {
226
+ let keyName = name;
227
+ for (let i = 0; i < groupPairs.length; i++) {
228
+ keyName += `->${row[i + 2]}`;
229
+ }
230
+ if (!acc[keyName]) {
231
+ acc[keyName] = {
232
+ id: keyName,
233
+ name,
234
+ service: `${row[2]} (Azure)`,
235
+ category: getCategoryByServiceName(row[2], categoryMappings),
236
+ provider: "Azure",
237
+ reports: {},
238
+ ...tagKeyValues
239
+ };
240
+ }
241
+ if (!moment__default.default(row[1]).isBefore(moment__default.default(parseInt(query.startTime, 10)))) {
242
+ acc[keyName].reports[row[1].substring(0, 7)] = parseFloat(
243
+ row[0]
244
+ );
245
+ }
246
+ return acc;
247
+ },
248
+ {}
249
+ );
250
+ Object.values(transformedData).map((value) => {
251
+ results.push(value);
252
+ });
253
+ } catch (e) {
254
+ throw new Error(e.message);
255
+ }
256
+ })();
257
+ promises.push(promise);
258
+ }
259
+ await Promise.all(promises);
260
+ return results;
261
+ }
262
+ }
263
+
264
+ async function createRouter(options) {
265
+ const { logger, config, cache, database } = options;
266
+ const router = Router__default.default();
267
+ router.use(express__default.default.json());
268
+ const azureClient = AzureClient.create(config, database);
269
+ const awsClient = AwsClient.create(config, database);
270
+ const cloudClients = [azureClient, awsClient];
271
+ router.get("/health", (_, response) => {
272
+ logger.info("PONG!");
273
+ response.json({ status: "ok" });
274
+ });
275
+ router.get("/reports", async (request, response) => {
276
+ const filters = request.query.filters;
277
+ const groups = request.query.groups;
278
+ const granularity = request.query.granularity;
279
+ const startTime = request.query.startTime;
280
+ const endTime = request.query.endTime;
281
+ const promises = [];
282
+ const results = [];
283
+ cloudClients.forEach(async (client) => {
284
+ const fetchCloudCosts = (async () => {
285
+ const cacheKey = [
286
+ client.constructor.name,
287
+ filters,
288
+ groups,
289
+ granularity,
290
+ startTime,
291
+ endTime
292
+ ].join("_");
293
+ const cachedCosts = await cache.get(cacheKey);
294
+ if (cachedCosts) {
295
+ logger.debug(`${client.constructor.name} costs from cache`);
296
+ cachedCosts.forEach((cost) => {
297
+ results.push(cost);
298
+ });
299
+ } else {
300
+ const costs = await client.fetchCostsFromCloud({
301
+ filters,
302
+ groups,
303
+ granularity,
304
+ startTime,
305
+ endTime
306
+ });
307
+ await cache.set(cacheKey, costs, {
308
+ ttl: 60 * 60 * 2 * 1e3
309
+ });
310
+ costs.forEach((cost) => {
311
+ results.push(cost);
312
+ });
313
+ }
314
+ })();
315
+ promises.push(fetchCloudCosts);
316
+ });
317
+ await Promise.all(promises);
318
+ response.json({ data: results, status: "ok" });
319
+ });
320
+ router.use(backendCommon.errorHandler());
321
+ return router;
322
+ }
323
+
324
+ const infraWalletPlugin = backendPluginApi.createBackendPlugin({
325
+ pluginId: "infrawallet",
326
+ register(env) {
327
+ env.registerInit({
328
+ deps: {
329
+ httpRouter: backendPluginApi.coreServices.httpRouter,
330
+ logger: backendPluginApi.coreServices.logger,
331
+ config: backendPluginApi.coreServices.rootConfig,
332
+ cache: backendPluginApi.coreServices.cache,
333
+ database: backendPluginApi.coreServices.database
334
+ },
335
+ async init({ httpRouter, logger, config, cache, database }) {
336
+ var _a;
337
+ const client = await database.getClient();
338
+ const migrationsDir = backendPluginApi.resolvePackagePath(
339
+ "@electrolux-oss/plugin-infrawallet-backend",
340
+ "migrations"
341
+ );
342
+ if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
343
+ await client.migrate.latest({
344
+ directory: migrationsDir
345
+ });
346
+ }
347
+ const category_mappings_count = await client("category_mappings").count(
348
+ "id as c"
349
+ );
350
+ if (category_mappings_count[0].c === 0 || category_mappings_count[0].c === "0") {
351
+ const seedsDir = backendPluginApi.resolvePackagePath(
352
+ "@electrolux-oss/plugin-infrawallet-backend",
353
+ "seeds"
354
+ );
355
+ await client.seed.run({ directory: seedsDir });
356
+ }
357
+ httpRouter.use(
358
+ await createRouter({
359
+ logger,
360
+ config,
361
+ cache,
362
+ database
363
+ })
364
+ );
365
+ httpRouter.addAuthPolicy({
366
+ path: "/health",
367
+ allow: "unauthenticated"
368
+ });
369
+ }
370
+ });
371
+ }
372
+ });
373
+
374
+ exports.createRouter = createRouter;
375
+ exports.default = infraWalletPlugin;
376
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/service/functions.ts","../src/service/AwsClient.ts","../src/service/AzureClient.ts","../src/service/router.ts","../src/plugin.ts"],"sourcesContent":["import { DatabaseService } from '@backstage/backend-plugin-api';\nimport { CategoryMapping } from './types';\n\nexport async function getCategoryMappings(\n database: DatabaseService,\n provider: string,\n): Promise<{ [category: string]: string[] }> {\n const result: { [category: string]: string[] } = {};\n const client = await database.getClient();\n const mappings = await client\n .where({ provider: provider })\n .select()\n .from<CategoryMapping>('category_mappings');\n mappings.forEach(mapping => {\n if (typeof mapping.cloud_service_names === 'string') {\n // just in case if the database such as sqlite does not support JSON column\n result[mapping.category] = JSON.parse(mapping.cloud_service_names);\n } else {\n result[mapping.category] = mapping.cloud_service_names;\n }\n });\n return result;\n}\n\nexport function getCategoryByServiceName(\n serviceName: string,\n categoryMappings: { [category: string]: string[] },\n): string {\n for (const key of Object.keys(categoryMappings)) {\n const serviceNames = categoryMappings[key];\n if (serviceNames && serviceNames.includes(serviceName)) {\n return key;\n }\n }\n\n return 'Uncategorized';\n}\n","import {\n CostExplorerClient,\n GetCostAndUsageCommand,\n Granularity,\n} from '@aws-sdk/client-cost-explorer';\nimport { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';\nimport { DatabaseService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { reduce } from 'lodash';\nimport moment from 'moment';\nimport { CostQuery, InfraWalletApi, Report } from './InfraWalletApi';\nimport { getCategoryMappings, getCategoryByServiceName } from './functions';\n\nexport class AwsClient implements InfraWalletApi {\n static create(config: Config, database: DatabaseService) {\n return new AwsClient(config, database);\n }\n\n constructor(\n private readonly config: Config,\n private readonly database: DatabaseService,\n ) {}\n\n async fetchCostsFromCloud(query: CostQuery): Promise<Report[]> {\n const conf = this.config.getOptionalConfigArray(\n 'backend.infraWallet.integrations.aws',\n );\n if (!conf) {\n return [];\n }\n\n const promises = [];\n const results = [];\n const groupPairs = [];\n query.groups.split(',').forEach(group => {\n if (group.includes(':')) {\n const [type, name] = group.split(':');\n groupPairs.push({ type, name });\n }\n });\n\n for (const c of conf) {\n const name = c.getOptionalString('name');\n const accountId = c.getOptionalString('accountId');\n const assumedRoleName = c.getOptionalString('assumedRoleName');\n const accessKeyId = c.getOptionalString('accessKeyId');\n const accessKeySecret = c.getOptionalString('accessKeySecret');\n const tags = c.getOptionalStringArray('tags');\n const tagKeyValues: { [key: string]: string } = {};\n tags?.forEach(tag => {\n const [k, v] = tag.split(':');\n tagKeyValues[k.trim()] = v.trim();\n });\n const categoryMappings = await getCategoryMappings(this.database, 'aws');\n\n const promise = (async () => {\n const client = new STSClient({\n region: 'us-east-1',\n credentials: {\n accessKeyId: accessKeyId as string,\n secretAccessKey: accessKeySecret as string,\n },\n });\n const commandInput = {\n // AssumeRoleRequest\n RoleArn: `arn:aws:iam::${accountId}:role/${assumedRoleName}`,\n RoleSessionName: 'AssumeRoleSession1',\n };\n const assumeRoleCommand = new AssumeRoleCommand(commandInput);\n const assumeRoleResponse = await client.send(assumeRoleCommand);\n // init aws cost explorer client\n const awsCeClient = new CostExplorerClient({\n region: 'us-east-1',\n credentials: {\n accessKeyId: assumeRoleResponse.Credentials?.AccessKeyId as string,\n secretAccessKey: assumeRoleResponse.Credentials\n ?.SecretAccessKey as string,\n sessionToken: assumeRoleResponse.Credentials\n ?.SessionToken as string,\n },\n });\n\n // query this aws account's cost and usage using @aws-sdk/client-cost-explorer\n const input = {\n TimePeriod: {\n Start: moment(parseInt(query.startTime, 10)).format('YYYY-MM-DD'),\n End: moment(parseInt(query.endTime, 10)).format('YYYY-MM-DD'),\n },\n Granularity: query.granularity.toUpperCase() as Granularity,\n Filter: { Dimensions: { Key: 'RECORD_TYPE', Values: ['Usage'] } },\n GroupBy: [{ Type: 'DIMENSION', Key: 'SERVICE' }],\n Metrics: ['UnblendedCost'],\n };\n const getCostCommand = new GetCostAndUsageCommand(input);\n const costAndusageResponse = await awsCeClient.send(getCostCommand);\n\n const transformedData = reduce(\n costAndusageResponse.ResultsByTime,\n (acc, row) => {\n const period = row.TimePeriod.Start.substring(0, 7);\n row.Groups.forEach(group => {\n const keyName = `${name}_${group.Keys[0]}`;\n\n if (!acc[keyName]) {\n acc[keyName] = {\n id: keyName,\n name: name,\n service: `${group.Keys[0]} (AWS)`,\n category: getCategoryByServiceName(\n group.Keys[0],\n categoryMappings,\n ),\n provider: 'AWS',\n reports: {},\n ...tagKeyValues,\n };\n }\n\n acc[keyName].reports[period] = parseFloat(\n group.Metrics.UnblendedCost.Amount,\n );\n });\n return acc;\n },\n {},\n );\n\n Object.values(transformedData).map((value: any) => {\n results.push(value);\n });\n })();\n promises.push(promise);\n }\n await Promise.all(promises);\n return results;\n }\n}\n","import { CostManagementClient } from '@azure/arm-costmanagement';\nimport { ClientSecretCredential } from '@azure/identity';\nimport { DatabaseService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { reduce } from 'lodash';\nimport moment from 'moment';\nimport { CostQuery, InfraWalletApi, Report } from './InfraWalletApi';\nimport { getCategoryMappings, getCategoryByServiceName } from './functions';\n\nexport class AzureClient implements InfraWalletApi {\n static create(config: Config, database: DatabaseService) {\n return new AzureClient(config, database);\n }\n\n constructor(\n private readonly config: Config,\n private readonly database: DatabaseService,\n ) {}\n\n async queryAzureCostExplorer(\n azureClient: any,\n subscription: string,\n granularity: string,\n groups: { type: string; name: string }[],\n startDate: moment.Moment,\n endDate: moment.Moment,\n ) {\n const scope = `/subscriptions/${subscription}`;\n\n const query = {\n type: 'ActualCost',\n dataset: {\n granularity: granularity,\n aggregation: { totalCostUSD: { name: 'CostUSD', function: 'Sum' } },\n grouping: groups,\n },\n timeframe: 'Custom',\n timePeriod: {\n from: startDate.toISOString(),\n to: endDate.toISOString(),\n },\n };\n\n const result = await azureClient.query.usage(scope, query);\n return result;\n }\n\n async fetchCostsFromCloud(query: CostQuery): Promise<Report[]> {\n const conf = this.config.getOptionalConfigArray(\n 'backend.infraWallet.integrations.azure',\n );\n if (!conf) {\n return [];\n }\n\n const promises = [];\n const results = [];\n\n const groupPairs = [{ type: 'Dimension', name: 'ServiceName' }];\n for (const c of conf) {\n const name = c.getOptionalString('name');\n const subscriptionId = c.getOptionalString('subscriptionId');\n const tenantId = c.getOptionalString('tenantId');\n const clientId = c.getOptionalString('clientId');\n const clientSecret = c.getOptionalString('clientSecret');\n const credential = new ClientSecretCredential(\n tenantId as string,\n clientId as string,\n clientSecret as string,\n );\n const client = new CostManagementClient(credential);\n const tags = c.getOptionalStringArray('tags');\n const tagKeyValues: { [key: string]: string } = {};\n tags?.forEach(tag => {\n const [k, v] = tag.split(':');\n tagKeyValues[k.trim()] = v.trim();\n });\n const categoryMappings = await getCategoryMappings(\n this.database,\n 'azure',\n );\n\n const promise = (async () => {\n try {\n const costResponse = await this.queryAzureCostExplorer(\n client,\n subscriptionId as string,\n query.granularity,\n groupPairs,\n moment(parseInt(query.startTime, 10)),\n moment(parseInt(query.endTime, 10)),\n );\n\n const transformedData = reduce(\n costResponse.rows,\n (acc, row) => {\n let keyName = name;\n for (let i = 0; i < groupPairs.length; i++) {\n keyName += `->${row[i + 2]}`;\n }\n\n if (!acc[keyName]) {\n acc[keyName] = {\n id: keyName,\n name: name,\n service: `${row[2]} (Azure)`,\n category: getCategoryByServiceName(row[2], categoryMappings),\n provider: 'Azure',\n reports: {},\n ...tagKeyValues,\n };\n }\n\n if (\n !moment(row[1]).isBefore(moment(parseInt(query.startTime, 10)))\n ) {\n acc[keyName].reports[row[1].substring(0, 7)] = parseFloat(\n row[0],\n );\n }\n return acc;\n },\n {},\n );\n\n Object.values(transformedData).map((value: any) => {\n results.push(value);\n });\n } catch (e) {\n throw new Error(e.message);\n }\n })();\n promises.push(promise);\n }\n await Promise.all(promises);\n return results;\n }\n}\n","import { errorHandler } from '@backstage/backend-common';\nimport {\n CacheService,\n DatabaseService,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { JsonArray } from '@backstage/types';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { AwsClient } from './AwsClient';\nimport { AzureClient } from './AzureClient';\nimport { InfraWalletApi } from './InfraWalletApi';\n\nexport interface RouterOptions {\n logger: LoggerService;\n config: Config;\n cache: CacheService;\n database: DatabaseService;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, config, cache, database } = options;\n\n const router = Router();\n router.use(express.json());\n\n const azureClient = AzureClient.create(config, database);\n const awsClient = AwsClient.create(config, database);\n const cloudClients: InfraWalletApi[] = [azureClient, awsClient];\n\n router.get('/health', (_, response) => {\n logger.info('PONG!');\n response.json({ status: 'ok' });\n });\n\n router.get('/reports', async (request, response) => {\n const filters = request.query.filters as string;\n const groups = request.query.groups as string;\n const granularity = request.query.granularity as string;\n const startTime = request.query.startTime as string;\n const endTime = request.query.endTime as string;\n const promises = [];\n const results = [];\n\n cloudClients.forEach(async client => {\n const fetchCloudCosts = (async () => {\n const cacheKey = [\n client.constructor.name,\n filters,\n groups,\n granularity,\n startTime,\n endTime,\n ].join('_');\n const cachedCosts = (await cache.get(cacheKey)) as JsonArray;\n if (cachedCosts) {\n logger.debug(`${client.constructor.name} costs from cache`);\n cachedCosts.forEach(cost => {\n results.push(cost);\n });\n } else {\n const costs = await client.fetchCostsFromCloud({\n filters: filters,\n groups: groups,\n granularity: granularity,\n startTime: startTime,\n endTime: endTime,\n });\n await cache.set(cacheKey, costs, {\n ttl: 60 * 60 * 2 * 1000,\n }); // cache for 2 hours\n costs.forEach(cost => {\n results.push(cost);\n });\n }\n })();\n promises.push(fetchCloudCosts);\n });\n\n await Promise.all(promises);\n\n response.json({ data: results, status: 'ok' });\n });\n\n router.use(errorHandler());\n return router;\n}\n","import { resolvePackagePath } from '@backstage/backend-plugin-api';\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\n\n/**\n * infraWalletPlugin backend plugin\n *\n * @public\n */\nexport const infraWalletPlugin = createBackendPlugin({\n pluginId: 'infrawallet',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n cache: coreServices.cache,\n database: coreServices.database,\n },\n async init({ httpRouter, logger, config, cache, database }) {\n // check database migrations\n const client = await database.getClient();\n const migrationsDir = resolvePackagePath(\n '@electrolux-oss/plugin-infrawallet-backend',\n 'migrations',\n );\n if (!database.migrations?.skip) {\n await client.migrate.latest({\n directory: migrationsDir,\n });\n }\n\n // if there are no category mappings, seed the database\n const category_mappings_count = await client('category_mappings').count(\n 'id as c',\n );\n if (\n category_mappings_count[0].c === 0 ||\n category_mappings_count[0].c === '0'\n ) {\n const seedsDir = resolvePackagePath(\n '@electrolux-oss/plugin-infrawallet-backend',\n 'seeds',\n );\n await client.seed.run({ directory: seedsDir });\n }\n\n httpRouter.use(\n await createRouter({\n logger,\n config,\n cache,\n database,\n }),\n );\n httpRouter.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["STSClient","AssumeRoleCommand","CostExplorerClient","moment","GetCostAndUsageCommand","reduce","ClientSecretCredential","CostManagementClient","Router","express","errorHandler","createBackendPlugin","coreServices","resolvePackagePath"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+ import { LoggerService, CacheService, DatabaseService } from '@backstage/backend-plugin-api';
3
+ import { Config } from '@backstage/config';
4
+ import express from 'express';
5
+
6
+ interface RouterOptions {
7
+ logger: LoggerService;
8
+ config: Config;
9
+ cache: CacheService;
10
+ database: DatabaseService;
11
+ }
12
+ declare function createRouter(options: RouterOptions): Promise<express.Router>;
13
+
14
+ /**
15
+ * infraWalletPlugin backend plugin
16
+ *
17
+ * @public
18
+ */
19
+ declare const infraWalletPlugin: () => _backstage_backend_plugin_api.BackendFeature;
20
+
21
+ export { type RouterOptions, createRouter, infraWalletPlugin as default };
@@ -0,0 +1,46 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param { import("knex").Knex } knex
5
+ * @returns { Promise<void> }
6
+ */
7
+ exports.up = async function up(knex) {
8
+ return (
9
+ knex.schema
10
+ //
11
+ // category mappings
12
+ //
13
+ .createTable('category_mappings', table => {
14
+ table.comment(
15
+ 'Category mapping configurations for different cloud service names',
16
+ );
17
+ table
18
+ .uuid('id')
19
+ .defaultTo(knex.fn.uuid())
20
+ .primary()
21
+ .notNullable()
22
+ .comment('Auto-generated ID of a mapping');
23
+ table
24
+ .string('provider')
25
+ .notNullable()
26
+ .comment('The name of a cloud provider');
27
+ table
28
+ .string('category')
29
+ .notNullable()
30
+ .comment('The name of a category');
31
+ table
32
+ .json('cloud_service_names') // if a database such as sqlite does not support json column, the data will be stored as plain text
33
+ .comment(
34
+ 'The cloud services that belong to this category (in json format like `["service1", "service2"]`)',
35
+ );
36
+ })
37
+ );
38
+ };
39
+
40
+ /**
41
+ * @param { import("knex").Knex } knex
42
+ * @returns { Promise<void> }
43
+ */
44
+ exports.down = async function down(knex) {
45
+ return knex.schema.dropTable('category_mappings');
46
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@electrolux-oss/plugin-infrawallet-backend",
3
+ "version": "0.1.0",
4
+ "backstage": {
5
+ "role": "backend-plugin"
6
+ },
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "main": "dist/index.cjs.js",
10
+ "types": "dist/index.d.ts"
11
+ },
12
+ "homepage": "https://opensource.electrolux.one",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/electrolux-oss/infrawallet",
16
+ "directory": "plugins/infrawallet-backend"
17
+ },
18
+ "license": "Apache-2.0",
19
+ "main": "dist/index.cjs.js",
20
+ "types": "dist/index.d.ts",
21
+ "files": [
22
+ "dist",
23
+ "migrations",
24
+ "seeds",
25
+ "config.d.ts"
26
+ ],
27
+ "scripts": {
28
+ "build": "backstage-cli package build",
29
+ "clean": "backstage-cli package clean",
30
+ "lint": "backstage-cli package lint",
31
+ "prepack": "backstage-cli package prepack",
32
+ "postpack": "backstage-cli package postpack",
33
+ "start": "backstage-cli package start",
34
+ "test": "backstage-cli package test"
35
+ },
36
+ "dependencies": {
37
+ "@aws-sdk/client-cost-explorer": "^3.554.0",
38
+ "@aws-sdk/client-sts": "^3.554.0",
39
+ "@azure/arm-costmanagement": "1.0.0-beta.1",
40
+ "@azure/identity": "4.1.0",
41
+ "@backstage/backend-common": "^0.22.0",
42
+ "@backstage/backend-defaults": "^0.2.18",
43
+ "@backstage/backend-plugin-api": "^0.6.18",
44
+ "@backstage/config": "^1.2.0",
45
+ "@backstage/types": "^1.1.1",
46
+ "@types/express": "*",
47
+ "express": "^4.17.1",
48
+ "express-promise-router": "^4.1.0",
49
+ "lodash": "^4.17.21",
50
+ "moment": "2.29.4",
51
+ "node-fetch": "^2.6.7",
52
+ "winston": "^3.2.1",
53
+ "yn": "^4.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@backstage/cli": "^0.26.5",
57
+ "@backstage/plugin-auth-backend": "^0.22.5",
58
+ "@backstage/plugin-auth-backend-module-guest-provider": "^0.1.4",
59
+ "@types/supertest": "^2.0.8",
60
+ "msw": "^1.0.0",
61
+ "supertest": "^6.2.4"
62
+ },
63
+ "configSchema": "config.d.ts"
64
+ }
package/seeds/init.js ADDED
@@ -0,0 +1,189 @@
1
+ exports.seed = async function (knex) {
2
+ await knex('category_mappings').insert([
3
+ {
4
+ provider: 'azure',
5
+ category: 'Analytics',
6
+ // make it compatible with sqlite
7
+ cloud_service_names: JSON.stringify([
8
+ 'Azure Monitor',
9
+ 'Log Analytics',
10
+ 'Network Watcher',
11
+ ]),
12
+ },
13
+ {
14
+ provider: 'azure',
15
+ category: 'Application-Integration',
16
+ cloud_service_names: JSON.stringify(['Logic Apps']),
17
+ },
18
+ {
19
+ provider: 'azure',
20
+ category: 'Compute',
21
+ cloud_service_names: JSON.stringify([
22
+ 'Virtual Machines',
23
+ 'Azure App Service',
24
+ 'Azure Data Factory v2',
25
+ 'Functions',
26
+ 'Azure Container Apps',
27
+ ]),
28
+ },
29
+ {
30
+ provider: 'azure',
31
+ category: 'Database',
32
+ cloud_service_names: JSON.stringify([
33
+ 'Azure Cosmos DB',
34
+ 'Azure Database for MySQL',
35
+ 'Redis Cache',
36
+ 'SQL Database',
37
+ ]),
38
+ },
39
+ {
40
+ provider: 'azure',
41
+ category: 'Developer-Tools',
42
+ cloud_service_names: JSON.stringify([
43
+ 'Azure DevOps',
44
+ 'Notification Hubs',
45
+ 'Visual Studio Subscription',
46
+ ]),
47
+ },
48
+ {
49
+ provider: 'azure',
50
+ category: 'Internet-of-Things',
51
+ cloud_service_names: JSON.stringify([
52
+ 'Event Grid',
53
+ 'Event Hubs',
54
+ 'IoT Hub',
55
+ ]),
56
+ },
57
+ {
58
+ provider: 'azure',
59
+ category: 'Machine-Learning',
60
+ cloud_service_names: JSON.stringify([
61
+ 'Azure Cognitive Search',
62
+ 'Azure Databricks',
63
+ 'Cognitive Services',
64
+ ]),
65
+ },
66
+ {
67
+ provider: 'azure',
68
+ category: 'Networking',
69
+ cloud_service_names: JSON.stringify([
70
+ 'Azure Front Door Service',
71
+ 'Bandwidth',
72
+ 'Content Delivery Network',
73
+ 'Load Balancer',
74
+ 'NAT Gateway',
75
+ 'Service Bus',
76
+ 'Traffic Manager',
77
+ 'Virtual Network',
78
+ 'Network Traversal',
79
+ 'Azure DNS',
80
+ 'VPN Gateway',
81
+ ]),
82
+ },
83
+ {
84
+ provider: 'azure',
85
+ category: 'Security-Identity-Compliance',
86
+ cloud_service_names: JSON.stringify([
87
+ 'Azure Firewall',
88
+ 'Key Vault',
89
+ 'Microsoft Defender for Cloud',
90
+ ]),
91
+ },
92
+ {
93
+ provider: 'azure',
94
+ category: 'Storage',
95
+ cloud_service_names: JSON.stringify(['Container Registry', 'Storage']),
96
+ },
97
+ {
98
+ provider: 'aws',
99
+ category: 'Analytics',
100
+ cloud_service_names: JSON.stringify([
101
+ 'AWS CloudTrail',
102
+ 'AWS X-Ray',
103
+ 'AmazonCloudWatch',
104
+ ]),
105
+ },
106
+ {
107
+ provider: 'aws',
108
+ category: 'Application-Integration',
109
+ cloud_service_names: JSON.stringify([
110
+ 'AWS Config',
111
+ 'Amazon API Gateway',
112
+ 'AWS Service Catalog',
113
+ ]),
114
+ },
115
+ {
116
+ provider: 'aws',
117
+ category: 'Cloud-Financial-Management',
118
+ cloud_service_names: JSON.stringify(['AWS Cost Explorer']),
119
+ },
120
+ {
121
+ provider: 'aws',
122
+ category: 'Compute',
123
+ cloud_service_names: JSON.stringify([
124
+ 'EC2 - Other',
125
+ 'Amazon Elastic Compute Cloud - Compute',
126
+ 'Amazon Elastic Container Service for Kubernetes',
127
+ 'AWS Lambda',
128
+ 'AWS Glue',
129
+ ]),
130
+ },
131
+ {
132
+ provider: 'aws',
133
+ category: 'Database',
134
+ cloud_service_names: JSON.stringify([
135
+ 'Amazon DynamoDB',
136
+ 'Amazon ElastiCache',
137
+ 'Amazon Kinesis',
138
+ 'Amazon Relational Database Service',
139
+ 'DynamoDB Accelerator (DAX)',
140
+ ]),
141
+ },
142
+ {
143
+ provider: 'aws',
144
+ category: 'Developer-Tools',
145
+ cloud_service_names: JSON.stringify([
146
+ 'Amazon Simple Notification Service',
147
+ 'Amazon Simple Queue Service',
148
+ 'AWS Migration Hub Refactor Spaces',
149
+ 'AWS CloudShell',
150
+ ]),
151
+ },
152
+ {
153
+ provider: 'aws',
154
+ category: 'Internet-of-Things',
155
+ cloud_service_names: JSON.stringify(['AWS IoT']),
156
+ },
157
+ {
158
+ provider: 'aws',
159
+ category: 'Networking',
160
+ cloud_service_names: JSON.stringify([
161
+ 'Amazon Elastic Load Balancing',
162
+ 'Amazon Managed Streaming for Apache Kafka',
163
+ 'Amazon Route 53',
164
+ 'Amazon Timestream',
165
+ 'Amazon Virtual Private Cloud',
166
+ ]),
167
+ },
168
+ {
169
+ provider: 'aws',
170
+ category: 'Security-Identity-Compliance',
171
+ cloud_service_names: JSON.stringify([
172
+ 'AWS Key Management Service',
173
+ 'AWS Secrets Manager',
174
+ 'AWS Security Hub',
175
+ 'Amazon GuardDuty',
176
+ 'AWS WAF',
177
+ ]),
178
+ },
179
+ {
180
+ provider: 'aws',
181
+ category: 'Storage',
182
+ cloud_service_names: JSON.stringify([
183
+ 'Amazon Simple Storage Service',
184
+ 'Amazon EC2 Container Registry (ECR)',
185
+ 'Amazon Glacier',
186
+ ]),
187
+ },
188
+ ]);
189
+ };