@electrolux-oss/plugin-infrawallet-backend 0.1.8 → 0.1.9

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 CHANGED
@@ -41,6 +41,11 @@ export interface Config {
41
41
  tags?: string[];
42
42
  },
43
43
  ];
44
+ mock?: [
45
+ {
46
+ name: string;
47
+ },
48
+ ];
44
49
  };
45
50
  metricProviders?: {
46
51
  datadog?: [
@@ -68,6 +73,11 @@ export interface Config {
68
73
  token: string;
69
74
  },
70
75
  ];
76
+ mock?: [
77
+ {
78
+ name: string;
79
+ },
80
+ ];
71
81
  };
72
82
  };
73
83
  };
package/dist/index.cjs.js CHANGED
@@ -13,16 +13,37 @@ var moment = require('moment');
13
13
  var armCostmanagement = require('@azure/arm-costmanagement');
14
14
  var coreRestPipeline = require('@azure/core-rest-pipeline');
15
15
  var identity = require('@azure/identity');
16
- var datadogApiClient = require('@datadog/datadog-api-client');
17
16
  var bigquery = require('@google-cloud/bigquery');
17
+ var datadogApiClient = require('@datadog/datadog-api-client');
18
18
  var fetch = require('node-fetch');
19
+ var fs = require('fs');
20
+ var upath = require('upath');
19
21
 
20
22
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
21
23
 
24
+ function _interopNamespaceCompat(e) {
25
+ if (e && typeof e === 'object' && 'default' in e) return e;
26
+ var n = Object.create(null);
27
+ if (e) {
28
+ Object.keys(e).forEach(function (k) {
29
+ if (k !== 'default') {
30
+ var d = Object.getOwnPropertyDescriptor(e, k);
31
+ Object.defineProperty(n, k, d.get ? d : {
32
+ enumerable: true,
33
+ get: function () { return e[k]; }
34
+ });
35
+ }
36
+ });
37
+ }
38
+ n.default = e;
39
+ return Object.freeze(n);
40
+ }
41
+
22
42
  var express__default = /*#__PURE__*/_interopDefaultCompat(express);
23
43
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
24
44
  var moment__default = /*#__PURE__*/_interopDefaultCompat(moment);
25
45
  var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
46
+ var upath__namespace = /*#__PURE__*/_interopNamespaceCompat(upath);
26
47
 
27
48
  async function getWallet(database, walletName) {
28
49
  const client = await database.getClient();
@@ -54,29 +75,25 @@ async function deleteWalletMetricSetting(database, walletSetting) {
54
75
  async function getCategoryMappings(database, provider) {
55
76
  const result = {};
56
77
  const client = await database.getClient();
57
- const default_mappings = await client.where({ provider: provider.toLowerCase() }).select().from("category_mappings_default");
58
- default_mappings.forEach((mapping) => {
59
- if (typeof mapping.cloud_service_names === "string") {
60
- JSON.parse(mapping.cloud_service_names).forEach((service) => {
61
- result[service] = mapping.category;
62
- });
63
- } else {
64
- mapping.cloud_service_names.forEach((service) => {
65
- result[service] = mapping.category;
66
- });
78
+ const defaultMappings = await client.where({ provider: provider.toLowerCase() }).select().from("category_mappings_default");
79
+ defaultMappings.forEach((mapping) => {
80
+ let services = mapping.cloud_service_names;
81
+ if (typeof services === "string") {
82
+ services = JSON.parse(services);
67
83
  }
84
+ services.forEach((service) => {
85
+ result[service] = mapping.category;
86
+ });
68
87
  });
69
- const override_mappings = await client.where({ provider }).select().from("category_mappings_override");
70
- override_mappings.forEach((mapping) => {
71
- if (typeof mapping.cloud_service_names === "string") {
72
- JSON.parse(mapping.cloud_service_names).forEach((service) => {
73
- result[service] = mapping.category;
74
- });
75
- } else {
76
- mapping.cloud_service_names.forEach((service) => {
77
- result[service] = mapping.category;
78
- });
88
+ const overrideMappings = await client.where({ provider }).select().from("category_mappings_override");
89
+ overrideMappings.forEach((mapping) => {
90
+ let services = mapping.cloud_service_names;
91
+ if (typeof services === "string") {
92
+ services = JSON.parse(services);
79
93
  }
94
+ services.forEach((service) => {
95
+ result[service] = mapping.category;
96
+ });
80
97
  });
81
98
  return result;
82
99
  }
@@ -84,6 +101,14 @@ function getCategoryByServiceName(serviceName, categoryMappings) {
84
101
  if (serviceName in categoryMappings) {
85
102
  return categoryMappings[serviceName];
86
103
  }
104
+ for (const service in categoryMappings) {
105
+ if (Object.hasOwn(categoryMappings, service)) {
106
+ const regex = new RegExp(service);
107
+ if (regex.test(serviceName)) {
108
+ return categoryMappings[service];
109
+ }
110
+ }
111
+ }
87
112
  return "Uncategorized";
88
113
  }
89
114
  async function getReportsFromCache(cache, provider, configKey, query) {
@@ -109,7 +134,8 @@ async function getMetricsFromCache(cache, provider, configKey, query) {
109
134
  query.startTime,
110
135
  query.endTime
111
136
  ].join("_");
112
- const cachedMetrics = await cache.get(cacheKey);
137
+ const crypto = require("crypto");
138
+ const cachedMetrics = await cache.get(crypto.createHash("md5").update(cacheKey).digest("hex"));
113
139
  return cachedMetrics;
114
140
  }
115
141
  async function setReportsToCache(cache, reports, provider, configKey, query, ttl) {
@@ -507,6 +533,98 @@ class AzureClient extends InfraWalletClient {
507
533
  }
508
534
  }
509
535
 
536
+ class GCPClient extends InfraWalletClient {
537
+ static create(config, database, cache, logger) {
538
+ return new GCPClient("GCP", config, database, cache, logger);
539
+ }
540
+ convertServiceName(serviceName) {
541
+ let convertedName = serviceName;
542
+ const prefixes = ["Google Cloud"];
543
+ for (const prefix of prefixes) {
544
+ if (serviceName.startsWith(prefix)) {
545
+ convertedName = serviceName.slice(prefix.length).trim();
546
+ }
547
+ }
548
+ return `${this.providerName}/${convertedName}`;
549
+ }
550
+ async initCloudClient(subAccountConfig) {
551
+ const keyFilePath = subAccountConfig.getString("keyFilePath");
552
+ const projectId = subAccountConfig.getString("projectId");
553
+ const options = {
554
+ keyFilename: keyFilePath,
555
+ projectId
556
+ };
557
+ const bigqueryClient = new bigquery.BigQuery(options);
558
+ return bigqueryClient;
559
+ }
560
+ async fetchCostsFromCloud(subAccountConfig, client, query) {
561
+ const projectId = subAccountConfig.getString("projectId");
562
+ const datasetId = subAccountConfig.getString("datasetId");
563
+ const tableId = subAccountConfig.getString("tableId");
564
+ try {
565
+ const periodFormat = query.granularity.toUpperCase() === "MONTHLY" ? "%Y-%m" : "%Y-%m-%d";
566
+ const sql = `
567
+ SELECT
568
+ project.name AS project,
569
+ service.description AS service,
570
+ FORMAT_TIMESTAMP('${periodFormat}', usage_start_time) AS period,
571
+ (SUM(CAST(cost AS NUMERIC)) + SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) AS c), 0))) AS total_cost
572
+ FROM
573
+ \`${projectId}.${datasetId}.${tableId}\`
574
+ WHERE
575
+ project.name IS NOT NULL
576
+ AND cost > 0
577
+ AND usage_start_time >= TIMESTAMP_MILLIS(${query.startTime})
578
+ AND usage_start_time <= TIMESTAMP_MILLIS(${query.endTime})
579
+ GROUP BY
580
+ project, service, period
581
+ ORDER BY
582
+ project, period, total_cost DESC`;
583
+ const [job] = await client.createQueryJob({
584
+ query: sql
585
+ });
586
+ const [rows] = await job.getQueryResults();
587
+ return rows;
588
+ } catch (err) {
589
+ throw new Error(err.message);
590
+ }
591
+ }
592
+ async transformCostsData(subAccountConfig, _query, costResponse, categoryMappings) {
593
+ const accountName = subAccountConfig.getString("name");
594
+ const tags = subAccountConfig.getOptionalStringArray("tags");
595
+ const tagKeyValues = {};
596
+ tags == null ? void 0 : tags.forEach((tag) => {
597
+ const [k, v] = tag.split(":");
598
+ tagKeyValues[k.trim()] = v.trim();
599
+ });
600
+ const transformedData = lodash.reduce(
601
+ costResponse,
602
+ (acc, row) => {
603
+ const period = row.period;
604
+ const keyName = `${accountName}_${row.project}_${row.service}`;
605
+ if (!acc[keyName]) {
606
+ acc[keyName] = {
607
+ id: keyName,
608
+ name: `${this.providerName}/${accountName}`,
609
+ service: this.convertServiceName(row.service),
610
+ category: getCategoryByServiceName(row.service, categoryMappings),
611
+ provider: this.providerName,
612
+ reports: {},
613
+ ...{ project: row.project },
614
+ // TODO: how should we handle the project field? for now, we add project name as a field in the report
615
+ ...tagKeyValues
616
+ // note that if there is a tag `project:foo` in config, it overrides the project field set above
617
+ };
618
+ }
619
+ acc[keyName].reports[period] = parseFloat(row.total_cost);
620
+ return acc;
621
+ },
622
+ {}
623
+ );
624
+ return Object.values(transformedData);
625
+ }
626
+ }
627
+
510
628
  class MetricProvider {
511
629
  constructor(providerName, config, database, cache, logger) {
512
630
  this.providerName = providerName;
@@ -637,98 +755,6 @@ class DatadogProvider extends MetricProvider {
637
755
  }
638
756
  }
639
757
 
640
- class GCPClient extends InfraWalletClient {
641
- static create(config, database, cache, logger) {
642
- return new GCPClient("GCP", config, database, cache, logger);
643
- }
644
- convertServiceName(serviceName) {
645
- let convertedName = serviceName;
646
- const prefixes = ["Google Cloud"];
647
- for (const prefix of prefixes) {
648
- if (serviceName.startsWith(prefix)) {
649
- convertedName = serviceName.slice(prefix.length).trim();
650
- }
651
- }
652
- return `${this.providerName}/${convertedName}`;
653
- }
654
- async initCloudClient(subAccountConfig) {
655
- const keyFilePath = subAccountConfig.getString("keyFilePath");
656
- const projectId = subAccountConfig.getString("projectId");
657
- const options = {
658
- keyFilename: keyFilePath,
659
- projectId
660
- };
661
- const bigqueryClient = new bigquery.BigQuery(options);
662
- return bigqueryClient;
663
- }
664
- async fetchCostsFromCloud(subAccountConfig, client, query) {
665
- const projectId = subAccountConfig.getString("projectId");
666
- const datasetId = subAccountConfig.getString("datasetId");
667
- const tableId = subAccountConfig.getString("tableId");
668
- try {
669
- const periodFormat = query.granularity.toUpperCase() === "MONTHLY" ? "%Y-%m" : "%Y-%m-%d";
670
- const sql = `
671
- SELECT
672
- project.name AS project,
673
- service.description AS service,
674
- FORMAT_TIMESTAMP('${periodFormat}', usage_start_time) AS period,
675
- (SUM(CAST(cost AS NUMERIC)) + SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) AS c), 0))) AS total_cost
676
- FROM
677
- \`${projectId}.${datasetId}.${tableId}\`
678
- WHERE
679
- project.name IS NOT NULL
680
- AND cost > 0
681
- AND usage_start_time >= TIMESTAMP_MILLIS(${query.startTime})
682
- AND usage_start_time <= TIMESTAMP_MILLIS(${query.endTime})
683
- GROUP BY
684
- project, service, period
685
- ORDER BY
686
- project, period, total_cost DESC`;
687
- const [job] = await client.createQueryJob({
688
- query: sql
689
- });
690
- const [rows] = await job.getQueryResults();
691
- return rows;
692
- } catch (err) {
693
- throw new Error(err.message);
694
- }
695
- }
696
- async transformCostsData(subAccountConfig, _query, costResponse, categoryMappings) {
697
- const accountName = subAccountConfig.getString("name");
698
- const tags = subAccountConfig.getOptionalStringArray("tags");
699
- const tagKeyValues = {};
700
- tags == null ? void 0 : tags.forEach((tag) => {
701
- const [k, v] = tag.split(":");
702
- tagKeyValues[k.trim()] = v.trim();
703
- });
704
- const transformedData = lodash.reduce(
705
- costResponse,
706
- (acc, row) => {
707
- const period = row.period;
708
- const keyName = `${accountName}_${row.project}_${row.service}`;
709
- if (!acc[keyName]) {
710
- acc[keyName] = {
711
- id: keyName,
712
- name: `${this.providerName}/${accountName}`,
713
- service: this.convertServiceName(row.service),
714
- category: getCategoryByServiceName(row.service, categoryMappings),
715
- provider: this.providerName,
716
- reports: {},
717
- ...{ project: row.project },
718
- // TODO: how should we handle the project field? for now, we add project name as a field in the report
719
- ...tagKeyValues
720
- // note that if there is a tag `project:foo` in config, it overrides the project field set above
721
- };
722
- }
723
- acc[keyName].reports[period] = parseFloat(row.total_cost);
724
- return acc;
725
- },
726
- {}
727
- );
728
- return Object.values(transformedData);
729
- }
730
- }
731
-
732
758
  class GrafanaCloudProvider extends MetricProvider {
733
759
  static create(config, database, cache, logger) {
734
760
  return new GrafanaCloudProvider("GrafanaCloud", config, database, cache, logger);
@@ -787,14 +813,117 @@ class GrafanaCloudProvider extends MetricProvider {
787
813
  }
788
814
  }
789
815
 
816
+ class MockProvider extends MetricProvider {
817
+ static create(config, database, cache, logger) {
818
+ return new MockProvider("Mock", config, database, cache, logger);
819
+ }
820
+ async initProviderClient(_config) {
821
+ return null;
822
+ }
823
+ async fetchMetrics(_metricProviderConfig, _client, _query) {
824
+ return null;
825
+ }
826
+ async transformMetricData(_metricProviderConfig, query, _metricResponse) {
827
+ var _a, _b;
828
+ const transformedData = [];
829
+ const metricName = query.name;
830
+ let mockSettings = {};
831
+ try {
832
+ mockSettings = JSON.parse(query.query);
833
+ } catch (e) {
834
+ }
835
+ const minValue = (_a = mockSettings.min) != null ? _a : 0;
836
+ const maxValue = (_b = mockSettings.max) != null ? _b : 1e3;
837
+ const metric = {
838
+ id: metricName,
839
+ provider: this.providerName,
840
+ name: metricName,
841
+ reports: {}
842
+ };
843
+ let cursor = moment__default.default(parseInt(query.startTime, 10));
844
+ while (cursor <= moment__default.default(parseInt(query.endTime, 10))) {
845
+ const period = cursor.format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
846
+ metric.reports[period] = Math.floor(Math.random() * (maxValue - minValue) + minValue);
847
+ cursor = cursor.add(1, query.granularity === "daily" ? "days" : "months");
848
+ }
849
+ transformedData.push(metric);
850
+ return transformedData;
851
+ }
852
+ }
853
+
854
+ class MockClient extends InfraWalletClient {
855
+ static create(config, database, cache, logger) {
856
+ return new MockClient("mock", config, database, cache, logger);
857
+ }
858
+ async initCloudClient(config) {
859
+ this.logger.debug(`MockClient.initCloudClient called with config: ${JSON.stringify(config)}`);
860
+ return null;
861
+ }
862
+ async fetchCostsFromCloud(_subAccountConfig, _client, _query) {
863
+ return null;
864
+ }
865
+ async transformCostsData(_subAccountConfig, query, _costResponse, _categoryMappings) {
866
+ try {
867
+ const startD = moment__default.default.unix(Number(query.startTime) / 1e3);
868
+ let endD = moment__default.default.unix(Number(query.endTime) / 1e3);
869
+ const mockDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "mock");
870
+ const mockFilePath = upath__namespace.join(mockDir, "mock_response.json");
871
+ const data = await fs.promises.readFile(mockFilePath, "utf8");
872
+ const jsonData = JSON.parse(data);
873
+ const currentDate = moment__default.default();
874
+ if (endD.isAfter(currentDate)) {
875
+ this.logger.warn("End Date is in the future, adjusting to current date.");
876
+ endD = currentDate.clone();
877
+ endD.add(1, "day");
878
+ }
879
+ const processedData = await Promise.all(
880
+ jsonData.map(async (item) => {
881
+ item.reports = {};
882
+ const StartDate = moment__default.default(startD);
883
+ let step;
884
+ let dateFormat = "YYYY-MM";
885
+ if (query.granularity.toLowerCase() === "monthly") {
886
+ step = "months";
887
+ dateFormat = "YYYY-MM";
888
+ } else if (query.granularity.toLowerCase() === "daily") {
889
+ step = "days";
890
+ dateFormat = "YYYY-MM-DD";
891
+ }
892
+ while (StartDate.isBefore(endD)) {
893
+ const dateString = StartDate.format(dateFormat);
894
+ if (query.granularity.toLowerCase() === "monthly") {
895
+ item.reports[dateString] = this.getRandomValue(0.4 * 30, 33.3 * 30);
896
+ } else {
897
+ item.reports[dateString] = this.getRandomValue(0.4, 33.3);
898
+ }
899
+ StartDate.add(1, step);
900
+ }
901
+ return item;
902
+ })
903
+ );
904
+ return processedData;
905
+ } catch (err) {
906
+ this.logger.error("Error while reading a file", err);
907
+ throw err;
908
+ }
909
+ }
910
+ getRandomValue(min, max) {
911
+ const random = Math.random();
912
+ const amplifiedRandom = Math.pow(random, 3);
913
+ return amplifiedRandom * (max - min) + min;
914
+ }
915
+ }
916
+
790
917
  const COST_CLIENT_MAPPINGS = {
791
918
  aws: AwsClient,
792
919
  azure: AzureClient,
793
- gcp: GCPClient
920
+ gcp: GCPClient,
921
+ mock: MockClient
794
922
  };
795
923
  const METRIC_PROVIDER_MAPPINGS = {
796
924
  datadog: DatadogProvider,
797
- grafanacloud: GrafanaCloudProvider
925
+ grafanacloud: GrafanaCloudProvider,
926
+ mock: MockProvider
798
927
  };
799
928
 
800
929
  async function setUpDatabase(database) {