@backstage/plugin-kubernetes-backend 0.7.0-next.2 → 0.7.1-next.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/CHANGELOG.md +94 -0
- package/dist/index.cjs.js +367 -116
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/package.json +12 -10
package/dist/index.cjs.js
CHANGED
|
@@ -7,11 +7,14 @@ var Router = require('express-promise-router');
|
|
|
7
7
|
var luxon = require('luxon');
|
|
8
8
|
var errors = require('@backstage/errors');
|
|
9
9
|
var container = require('@google-cloud/container');
|
|
10
|
+
var catalogClient = require('@backstage/catalog-client');
|
|
11
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
10
12
|
var clientNode = require('@kubernetes/client-node');
|
|
11
13
|
var AWS = require('aws-sdk');
|
|
12
14
|
var aws4 = require('aws4');
|
|
13
15
|
var identity = require('@azure/identity');
|
|
14
16
|
var lodash = require('lodash');
|
|
17
|
+
var pluginAuthNode = require('@backstage/plugin-auth-node');
|
|
15
18
|
|
|
16
19
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
17
20
|
|
|
@@ -44,56 +47,60 @@ class ConfigClusterLocator {
|
|
|
44
47
|
this.clusterDetails = clusterDetails;
|
|
45
48
|
}
|
|
46
49
|
static fromConfig(config) {
|
|
47
|
-
return new ConfigClusterLocator(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const dashboardApp = c.getOptionalString("dashboardApp");
|
|
64
|
-
if (dashboardApp) {
|
|
65
|
-
clusterDetails.dashboardApp = dashboardApp;
|
|
66
|
-
}
|
|
67
|
-
if (c.has("dashboardParameters")) {
|
|
68
|
-
clusterDetails.dashboardParameters = c.get("dashboardParameters");
|
|
69
|
-
}
|
|
70
|
-
switch (authProvider) {
|
|
71
|
-
case "google": {
|
|
72
|
-
return clusterDetails;
|
|
73
|
-
}
|
|
74
|
-
case "aws": {
|
|
75
|
-
const assumeRole = c.getOptionalString("assumeRole");
|
|
76
|
-
const externalId = c.getOptionalString("externalId");
|
|
77
|
-
return { assumeRole, externalId, ...clusterDetails };
|
|
78
|
-
}
|
|
79
|
-
case "azure": {
|
|
80
|
-
return clusterDetails;
|
|
81
|
-
}
|
|
82
|
-
case "oidc": {
|
|
83
|
-
const oidcTokenProvider = c.getString("oidcTokenProvider");
|
|
84
|
-
return { oidcTokenProvider, ...clusterDetails };
|
|
50
|
+
return new ConfigClusterLocator(
|
|
51
|
+
config.getConfigArray("clusters").map((c) => {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
const authProvider = c.getString("authProvider");
|
|
54
|
+
const clusterDetails = {
|
|
55
|
+
name: c.getString("name"),
|
|
56
|
+
url: c.getString("url"),
|
|
57
|
+
serviceAccountToken: c.getOptionalString("serviceAccountToken"),
|
|
58
|
+
skipTLSVerify: (_a = c.getOptionalBoolean("skipTLSVerify")) != null ? _a : false,
|
|
59
|
+
skipMetricsLookup: (_b = c.getOptionalBoolean("skipMetricsLookup")) != null ? _b : false,
|
|
60
|
+
caData: c.getOptionalString("caData"),
|
|
61
|
+
authProvider
|
|
62
|
+
};
|
|
63
|
+
const dashboardUrl = c.getOptionalString("dashboardUrl");
|
|
64
|
+
if (dashboardUrl) {
|
|
65
|
+
clusterDetails.dashboardUrl = dashboardUrl;
|
|
85
66
|
}
|
|
86
|
-
|
|
87
|
-
|
|
67
|
+
const dashboardApp = c.getOptionalString("dashboardApp");
|
|
68
|
+
if (dashboardApp) {
|
|
69
|
+
clusterDetails.dashboardApp = dashboardApp;
|
|
88
70
|
}
|
|
89
|
-
|
|
90
|
-
|
|
71
|
+
if (c.has("dashboardParameters")) {
|
|
72
|
+
clusterDetails.dashboardParameters = c.get("dashboardParameters");
|
|
91
73
|
}
|
|
92
|
-
|
|
93
|
-
|
|
74
|
+
switch (authProvider) {
|
|
75
|
+
case "google": {
|
|
76
|
+
return clusterDetails;
|
|
77
|
+
}
|
|
78
|
+
case "aws": {
|
|
79
|
+
const assumeRole = c.getOptionalString("assumeRole");
|
|
80
|
+
const externalId = c.getOptionalString("externalId");
|
|
81
|
+
return { assumeRole, externalId, ...clusterDetails };
|
|
82
|
+
}
|
|
83
|
+
case "azure": {
|
|
84
|
+
return clusterDetails;
|
|
85
|
+
}
|
|
86
|
+
case "oidc": {
|
|
87
|
+
const oidcTokenProvider = c.getString("oidcTokenProvider");
|
|
88
|
+
return { oidcTokenProvider, ...clusterDetails };
|
|
89
|
+
}
|
|
90
|
+
case "serviceAccount": {
|
|
91
|
+
return clusterDetails;
|
|
92
|
+
}
|
|
93
|
+
case "googleServiceAccount": {
|
|
94
|
+
return clusterDetails;
|
|
95
|
+
}
|
|
96
|
+
default: {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`authProvider "${authProvider}" has no config associated with it`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
94
101
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
102
|
+
})
|
|
103
|
+
);
|
|
97
104
|
}
|
|
98
105
|
async getClusters() {
|
|
99
106
|
return this.clusterDetails;
|
|
@@ -147,12 +154,19 @@ class GkeClusterLocator {
|
|
|
147
154
|
};
|
|
148
155
|
const gkeClusterLocator = new GkeClusterLocator(options, client);
|
|
149
156
|
if (refreshInterval) {
|
|
150
|
-
runPeriodically(
|
|
157
|
+
runPeriodically(
|
|
158
|
+
() => gkeClusterLocator.refreshClusters(),
|
|
159
|
+
refreshInterval.toMillis()
|
|
160
|
+
);
|
|
151
161
|
}
|
|
152
162
|
return gkeClusterLocator;
|
|
153
163
|
}
|
|
154
164
|
static fromConfig(config, refreshInterval = void 0) {
|
|
155
|
-
return GkeClusterLocator.fromConfigWithClient(
|
|
165
|
+
return GkeClusterLocator.fromConfigWithClient(
|
|
166
|
+
config,
|
|
167
|
+
new container__namespace.v1.ClusterManagerClient(),
|
|
168
|
+
refreshInterval
|
|
169
|
+
);
|
|
156
170
|
}
|
|
157
171
|
async getClusters() {
|
|
158
172
|
var _a;
|
|
@@ -203,33 +217,96 @@ class GkeClusterLocator {
|
|
|
203
217
|
});
|
|
204
218
|
this.hasClusterDetails = true;
|
|
205
219
|
} catch (e) {
|
|
206
|
-
throw new errors.ForwardedError(
|
|
220
|
+
throw new errors.ForwardedError(
|
|
221
|
+
`There was an error retrieving clusters from GKE for projectId=${projectId} region=${region}`,
|
|
222
|
+
e
|
|
223
|
+
);
|
|
207
224
|
}
|
|
208
225
|
}
|
|
209
226
|
}
|
|
210
227
|
|
|
228
|
+
class CatalogClusterLocator {
|
|
229
|
+
constructor(catalogClient) {
|
|
230
|
+
this.catalogClient = catalogClient;
|
|
231
|
+
}
|
|
232
|
+
static fromConfig(catalogApi) {
|
|
233
|
+
return new CatalogClusterLocator(catalogApi);
|
|
234
|
+
}
|
|
235
|
+
async getClusters() {
|
|
236
|
+
const apiServerKey = `metadata.annotations.${catalogModel.ANNOTATION_KUBERNETES_API_SERVER}`;
|
|
237
|
+
const apiServerCaKey = `metadata.annotations.${catalogModel.ANNOTATION_KUBERNETES_API_SERVER_CA}`;
|
|
238
|
+
const authProviderKey = `metadata.annotations.${catalogModel.ANNOTATION_KUBERNETES_AUTH_PROVIDER}`;
|
|
239
|
+
const filter = {
|
|
240
|
+
kind: "Resource",
|
|
241
|
+
"spec.type": "kubernetes-cluster",
|
|
242
|
+
[apiServerKey]: catalogClient.CATALOG_FILTER_EXISTS,
|
|
243
|
+
[apiServerCaKey]: catalogClient.CATALOG_FILTER_EXISTS,
|
|
244
|
+
[authProviderKey]: catalogClient.CATALOG_FILTER_EXISTS
|
|
245
|
+
};
|
|
246
|
+
const clusters = await this.catalogClient.getEntities({
|
|
247
|
+
filter: [filter]
|
|
248
|
+
});
|
|
249
|
+
return clusters.items.map((entity) => {
|
|
250
|
+
const clusterDetails = {
|
|
251
|
+
name: entity.metadata.name,
|
|
252
|
+
url: entity.metadata.annotations[catalogModel.ANNOTATION_KUBERNETES_API_SERVER],
|
|
253
|
+
caData: entity.metadata.annotations[catalogModel.ANNOTATION_KUBERNETES_API_SERVER_CA],
|
|
254
|
+
authProvider: entity.metadata.annotations[catalogModel.ANNOTATION_KUBERNETES_AUTH_PROVIDER]
|
|
255
|
+
};
|
|
256
|
+
return clusterDetails;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
class LocalKubectlProxyClusterLocator {
|
|
262
|
+
constructor() {
|
|
263
|
+
this.clusterDetails = [
|
|
264
|
+
{
|
|
265
|
+
name: "local",
|
|
266
|
+
url: "http:/localhost:8001",
|
|
267
|
+
authProvider: "localKubectlProxy",
|
|
268
|
+
skipMetricsLookup: true
|
|
269
|
+
}
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
async getClusters() {
|
|
273
|
+
return this.clusterDetails;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
211
277
|
class CombinedClustersSupplier {
|
|
212
278
|
constructor(clusterSuppliers) {
|
|
213
279
|
this.clusterSuppliers = clusterSuppliers;
|
|
214
280
|
}
|
|
215
281
|
async getClusters() {
|
|
216
|
-
return await Promise.all(
|
|
282
|
+
return await Promise.all(
|
|
283
|
+
this.clusterSuppliers.map((supplier) => supplier.getClusters())
|
|
284
|
+
).then((res) => {
|
|
217
285
|
return res.flat();
|
|
218
286
|
}).catch((e) => {
|
|
219
287
|
throw e;
|
|
220
288
|
});
|
|
221
289
|
}
|
|
222
290
|
}
|
|
223
|
-
const getCombinedClusterSupplier = (rootConfig, refreshInterval = void 0) => {
|
|
291
|
+
const getCombinedClusterSupplier = (rootConfig, catalogClient, refreshInterval = void 0) => {
|
|
224
292
|
const clusterSuppliers = rootConfig.getConfigArray("kubernetes.clusterLocatorMethods").map((clusterLocatorMethod) => {
|
|
225
293
|
const type = clusterLocatorMethod.getString("type");
|
|
226
294
|
switch (type) {
|
|
295
|
+
case "catalog":
|
|
296
|
+
return CatalogClusterLocator.fromConfig(catalogClient);
|
|
297
|
+
case "localKubectlProxy":
|
|
298
|
+
return new LocalKubectlProxyClusterLocator();
|
|
227
299
|
case "config":
|
|
228
300
|
return ConfigClusterLocator.fromConfig(clusterLocatorMethod);
|
|
229
301
|
case "gke":
|
|
230
|
-
return GkeClusterLocator.fromConfig(
|
|
302
|
+
return GkeClusterLocator.fromConfig(
|
|
303
|
+
clusterLocatorMethod,
|
|
304
|
+
refreshInterval
|
|
305
|
+
);
|
|
231
306
|
default:
|
|
232
|
-
throw new Error(
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Unsupported kubernetes.clusterLocatorMethods: "${type}"`
|
|
309
|
+
);
|
|
233
310
|
}
|
|
234
311
|
});
|
|
235
312
|
return new CombinedClustersSupplier(clusterSuppliers);
|
|
@@ -290,12 +367,17 @@ class KubernetesClientProvider {
|
|
|
290
367
|
|
|
291
368
|
class GoogleKubernetesAuthTranslator {
|
|
292
369
|
async decorateClusterDetailsWithAuth(clusterDetails, authConfig) {
|
|
293
|
-
const clusterDetailsWithAuthToken = Object.assign(
|
|
370
|
+
const clusterDetailsWithAuthToken = Object.assign(
|
|
371
|
+
{},
|
|
372
|
+
clusterDetails
|
|
373
|
+
);
|
|
294
374
|
const authToken = authConfig.google;
|
|
295
375
|
if (authToken) {
|
|
296
376
|
clusterDetailsWithAuthToken.serviceAccountToken = authToken;
|
|
297
377
|
} else {
|
|
298
|
-
throw new Error(
|
|
378
|
+
throw new Error(
|
|
379
|
+
"Google token not found under auth.google in request body"
|
|
380
|
+
);
|
|
299
381
|
}
|
|
300
382
|
return clusterDetailsWithAuthToken;
|
|
301
383
|
}
|
|
@@ -377,21 +459,33 @@ class AwsIamKubernetesAuthTranslator {
|
|
|
377
459
|
return `k8s-aws-v1.${urlSafeBase64Url}`;
|
|
378
460
|
}
|
|
379
461
|
async decorateClusterDetailsWithAuth(clusterDetails) {
|
|
380
|
-
const clusterDetailsWithAuthToken = Object.assign(
|
|
381
|
-
|
|
462
|
+
const clusterDetailsWithAuthToken = Object.assign(
|
|
463
|
+
{},
|
|
464
|
+
clusterDetails
|
|
465
|
+
);
|
|
466
|
+
clusterDetailsWithAuthToken.serviceAccountToken = await this.getBearerToken(
|
|
467
|
+
clusterDetails.name,
|
|
468
|
+
clusterDetails.assumeRole,
|
|
469
|
+
clusterDetails.externalId
|
|
470
|
+
);
|
|
382
471
|
return clusterDetailsWithAuthToken;
|
|
383
472
|
}
|
|
384
473
|
}
|
|
385
474
|
|
|
386
475
|
class GoogleServiceAccountAuthTranslator {
|
|
387
476
|
async decorateClusterDetailsWithAuth(clusterDetails) {
|
|
388
|
-
const clusterDetailsWithAuthToken = Object.assign(
|
|
477
|
+
const clusterDetailsWithAuthToken = Object.assign(
|
|
478
|
+
{},
|
|
479
|
+
clusterDetails
|
|
480
|
+
);
|
|
389
481
|
const client = new container__namespace.v1.ClusterManagerClient();
|
|
390
482
|
const accessToken = await client.auth.getAccessToken();
|
|
391
483
|
if (accessToken) {
|
|
392
484
|
clusterDetailsWithAuthToken.serviceAccountToken = accessToken;
|
|
393
485
|
} else {
|
|
394
|
-
throw new Error(
|
|
486
|
+
throw new Error(
|
|
487
|
+
"Unable to obtain access token for the current Google Application Default Credentials"
|
|
488
|
+
);
|
|
395
489
|
}
|
|
396
490
|
return clusterDetailsWithAuthToken;
|
|
397
491
|
}
|
|
@@ -405,7 +499,10 @@ class AzureIdentityKubernetesAuthTranslator {
|
|
|
405
499
|
this.accessToken = { token: "", expiresOnTimestamp: 0 };
|
|
406
500
|
}
|
|
407
501
|
async decorateClusterDetailsWithAuth(clusterDetails) {
|
|
408
|
-
const clusterDetailsWithAuthToken = Object.assign(
|
|
502
|
+
const clusterDetailsWithAuthToken = Object.assign(
|
|
503
|
+
{},
|
|
504
|
+
clusterDetails
|
|
505
|
+
);
|
|
409
506
|
clusterDetailsWithAuthToken.serviceAccountToken = await this.getToken();
|
|
410
507
|
return clusterDetailsWithAuthToken;
|
|
411
508
|
}
|
|
@@ -449,16 +546,23 @@ class AzureIdentityKubernetesAuthTranslator {
|
|
|
449
546
|
class OidcKubernetesAuthTranslator {
|
|
450
547
|
async decorateClusterDetailsWithAuth(clusterDetails, authConfig) {
|
|
451
548
|
var _a;
|
|
452
|
-
const clusterDetailsWithAuthToken = Object.assign(
|
|
549
|
+
const clusterDetailsWithAuthToken = Object.assign(
|
|
550
|
+
{},
|
|
551
|
+
clusterDetails
|
|
552
|
+
);
|
|
453
553
|
const { oidcTokenProvider } = clusterDetails;
|
|
454
554
|
if (!oidcTokenProvider || oidcTokenProvider === "") {
|
|
455
|
-
throw new Error(
|
|
555
|
+
throw new Error(
|
|
556
|
+
`oidc authProvider requires a configured oidcTokenProvider`
|
|
557
|
+
);
|
|
456
558
|
}
|
|
457
559
|
const authToken = (_a = authConfig.oidc) == null ? void 0 : _a[oidcTokenProvider];
|
|
458
560
|
if (authToken) {
|
|
459
561
|
clusterDetailsWithAuthToken.serviceAccountToken = authToken;
|
|
460
562
|
} else {
|
|
461
|
-
throw new Error(
|
|
563
|
+
throw new Error(
|
|
564
|
+
`Auth token not found under oidc.${oidcTokenProvider} in request body`
|
|
565
|
+
);
|
|
462
566
|
}
|
|
463
567
|
return clusterDetailsWithAuthToken;
|
|
464
568
|
}
|
|
@@ -485,8 +589,13 @@ class KubernetesAuthTranslatorGenerator {
|
|
|
485
589
|
case "oidc": {
|
|
486
590
|
return new OidcKubernetesAuthTranslator();
|
|
487
591
|
}
|
|
592
|
+
case "localKubectlProxy": {
|
|
593
|
+
return new NoopKubernetesAuthTranslator();
|
|
594
|
+
}
|
|
488
595
|
default: {
|
|
489
|
-
throw new Error(
|
|
596
|
+
throw new Error(
|
|
597
|
+
`authProvider "${authProvider}" has no KubernetesAuthTranslator associated with it`
|
|
598
|
+
);
|
|
490
599
|
}
|
|
491
600
|
}
|
|
492
601
|
}
|
|
@@ -609,45 +718,66 @@ class KubernetesFanOutHandler {
|
|
|
609
718
|
auth,
|
|
610
719
|
customResources
|
|
611
720
|
}) {
|
|
612
|
-
return this.fanOutRequests(
|
|
721
|
+
return this.fanOutRequests(
|
|
722
|
+
entity,
|
|
723
|
+
auth,
|
|
724
|
+
/* @__PURE__ */ new Set(),
|
|
725
|
+
customResources
|
|
726
|
+
);
|
|
613
727
|
}
|
|
614
728
|
async getKubernetesObjectsByEntity({
|
|
615
729
|
entity,
|
|
616
730
|
auth
|
|
617
731
|
}) {
|
|
618
|
-
return this.fanOutRequests(
|
|
732
|
+
return this.fanOutRequests(
|
|
733
|
+
entity,
|
|
734
|
+
auth,
|
|
735
|
+
this.objectTypesToFetch,
|
|
736
|
+
this.customResources
|
|
737
|
+
);
|
|
619
738
|
}
|
|
620
739
|
async fanOutRequests(entity, auth, objectTypesToFetch, customResources) {
|
|
621
740
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
622
741
|
const entityName = ((_b = (_a = entity.metadata) == null ? void 0 : _a.annotations) == null ? void 0 : _b["backstage.io/kubernetes-id"]) || ((_c = entity.metadata) == null ? void 0 : _c.name);
|
|
623
742
|
const clusterDetailsDecoratedForAuth = await this.decorateClusterDetailsWithAuth(entity, auth);
|
|
624
|
-
this.logger.info(
|
|
743
|
+
this.logger.info(
|
|
744
|
+
`entity.metadata.name=${entityName} clusterDetails=[${clusterDetailsDecoratedForAuth.map((c) => c.name).join(", ")}]`
|
|
745
|
+
);
|
|
625
746
|
const labelSelector = ((_e = (_d = entity.metadata) == null ? void 0 : _d.annotations) == null ? void 0 : _e["backstage.io/kubernetes-label-selector"]) || `backstage.io/kubernetes-id=${entityName}`;
|
|
626
747
|
const namespace = (_g = (_f = entity.metadata) == null ? void 0 : _f.annotations) == null ? void 0 : _g["backstage.io/kubernetes-namespace"];
|
|
627
|
-
return Promise.all(
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
748
|
+
return Promise.all(
|
|
749
|
+
clusterDetailsDecoratedForAuth.map((clusterDetailsItem) => {
|
|
750
|
+
return this.fetcher.fetchObjectsForService({
|
|
751
|
+
serviceId: entityName,
|
|
752
|
+
clusterDetails: clusterDetailsItem,
|
|
753
|
+
objectTypesToFetch,
|
|
754
|
+
labelSelector,
|
|
755
|
+
customResources: customResources.map((c) => ({
|
|
756
|
+
...c,
|
|
757
|
+
objectType: "customresources"
|
|
758
|
+
})),
|
|
759
|
+
namespace
|
|
760
|
+
}).then((result) => this.getMetricsForPods(clusterDetailsItem, result)).then((r) => this.toClusterObjects(clusterDetailsItem, r));
|
|
761
|
+
})
|
|
762
|
+
).then(this.toObjectsByEntityResponse);
|
|
640
763
|
}
|
|
641
764
|
async decorateClusterDetailsWithAuth(entity, auth) {
|
|
642
765
|
const clusterDetails = await (await this.serviceLocator.getClustersByEntity(entity)).clusters;
|
|
643
|
-
return await Promise.all(
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
766
|
+
return await Promise.all(
|
|
767
|
+
clusterDetails.map((cd) => {
|
|
768
|
+
const kubernetesAuthTranslator = this.getAuthTranslator(cd.authProvider);
|
|
769
|
+
return kubernetesAuthTranslator.decorateClusterDetailsWithAuth(
|
|
770
|
+
cd,
|
|
771
|
+
auth
|
|
772
|
+
);
|
|
773
|
+
})
|
|
774
|
+
);
|
|
647
775
|
}
|
|
648
776
|
toObjectsByEntityResponse(clusterObjects) {
|
|
649
777
|
return {
|
|
650
|
-
items: clusterObjects.filter(
|
|
778
|
+
items: clusterObjects.filter(
|
|
779
|
+
(item) => item.errors !== void 0 && item.errors.length >= 1 || item.resources !== void 0 && item.resources.length >= 1 && item.resources.some((fr) => fr.resources.length >= 1)
|
|
780
|
+
)
|
|
651
781
|
};
|
|
652
782
|
}
|
|
653
783
|
toClusterObjects(clusterDetails, [result, metrics]) {
|
|
@@ -674,20 +804,27 @@ class KubernetesFanOutHandler {
|
|
|
674
804
|
if (clusterDetails.skipMetricsLookup) {
|
|
675
805
|
return [result, []];
|
|
676
806
|
}
|
|
677
|
-
const namespaces = new Set(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
807
|
+
const namespaces = new Set(
|
|
808
|
+
result.responses.filter(isPodFetchResponse).flatMap((r) => r.resources).map((p) => {
|
|
809
|
+
var _a;
|
|
810
|
+
return (_a = p.metadata) == null ? void 0 : _a.namespace;
|
|
811
|
+
}).filter(isString)
|
|
812
|
+
);
|
|
813
|
+
const podMetrics = Array.from(namespaces).map(
|
|
814
|
+
(ns) => this.fetcher.fetchPodMetricsByNamespace(clusterDetails, ns)
|
|
815
|
+
);
|
|
682
816
|
return Promise.all([result, Promise.all(podMetrics)]);
|
|
683
817
|
}
|
|
684
818
|
getAuthTranslator(provider) {
|
|
685
819
|
if (this.authTranslators[provider]) {
|
|
686
820
|
return this.authTranslators[provider];
|
|
687
821
|
}
|
|
688
|
-
this.authTranslators[provider] = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance(
|
|
689
|
-
|
|
690
|
-
|
|
822
|
+
this.authTranslators[provider] = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance(
|
|
823
|
+
provider,
|
|
824
|
+
{
|
|
825
|
+
logger: this.logger
|
|
826
|
+
}
|
|
827
|
+
);
|
|
691
828
|
return this.authTranslators[provider];
|
|
692
829
|
}
|
|
693
830
|
}
|
|
@@ -725,18 +862,28 @@ class KubernetesClientBasedFetcher {
|
|
|
725
862
|
}
|
|
726
863
|
fetchObjectsForService(params) {
|
|
727
864
|
const fetchResults = Array.from(params.objectTypesToFetch).concat(params.customResources).map((toFetch) => {
|
|
728
|
-
return this.fetchResource(
|
|
865
|
+
return this.fetchResource(
|
|
866
|
+
params.clusterDetails,
|
|
867
|
+
toFetch,
|
|
868
|
+
params.labelSelector || `backstage.io/kubernetes-id=${params.serviceId}`,
|
|
869
|
+
toFetch.objectType,
|
|
870
|
+
params.namespace
|
|
871
|
+
).catch(this.captureKubernetesErrorsRethrowOthers.bind(this));
|
|
729
872
|
});
|
|
730
873
|
return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);
|
|
731
874
|
}
|
|
732
875
|
fetchPodMetricsByNamespace(clusterDetails, namespace) {
|
|
733
876
|
const metricsClient = this.kubernetesClientProvider.getMetricsClient(clusterDetails);
|
|
734
|
-
const coreApi = this.kubernetesClientProvider.getCoreClientByClusterDetails(
|
|
877
|
+
const coreApi = this.kubernetesClientProvider.getCoreClientByClusterDetails(
|
|
878
|
+
clusterDetails
|
|
879
|
+
);
|
|
735
880
|
return clientNode.topPods(coreApi, metricsClient, namespace);
|
|
736
881
|
}
|
|
737
882
|
captureKubernetesErrorsRethrowOthers(e) {
|
|
738
883
|
if (e.response && e.response.statusCode) {
|
|
739
|
-
this.logger.warn(
|
|
884
|
+
this.logger.warn(
|
|
885
|
+
`statusCode=${e.response.statusCode} for resource ${e.response.request.uri.pathname} body=[${JSON.stringify(e.response.body)}]`
|
|
886
|
+
);
|
|
740
887
|
return {
|
|
741
888
|
errorType: statusCodeToErrorType(e.response.statusCode),
|
|
742
889
|
statusCode: e.response.statusCode,
|
|
@@ -751,14 +898,33 @@ class KubernetesClientBasedFetcher {
|
|
|
751
898
|
requestOptions.uri = requestOptions.uri.replace("/apis//v1/", "/api/v1/");
|
|
752
899
|
});
|
|
753
900
|
if (namespace) {
|
|
754
|
-
return customObjects.listNamespacedCustomObject(
|
|
901
|
+
return customObjects.listNamespacedCustomObject(
|
|
902
|
+
resource.group,
|
|
903
|
+
resource.apiVersion,
|
|
904
|
+
namespace,
|
|
905
|
+
resource.plural,
|
|
906
|
+
"",
|
|
907
|
+
false,
|
|
908
|
+
"",
|
|
909
|
+
"",
|
|
910
|
+
labelSelector
|
|
911
|
+
).then((r) => {
|
|
755
912
|
return {
|
|
756
913
|
type: objectType,
|
|
757
914
|
resources: r.body.items
|
|
758
915
|
};
|
|
759
916
|
});
|
|
760
917
|
}
|
|
761
|
-
return customObjects.listClusterCustomObject(
|
|
918
|
+
return customObjects.listClusterCustomObject(
|
|
919
|
+
resource.group,
|
|
920
|
+
resource.apiVersion,
|
|
921
|
+
resource.plural,
|
|
922
|
+
"",
|
|
923
|
+
false,
|
|
924
|
+
"",
|
|
925
|
+
"",
|
|
926
|
+
labelSelector
|
|
927
|
+
).then((r) => {
|
|
762
928
|
return {
|
|
763
929
|
type: objectType,
|
|
764
930
|
resources: r.body.items
|
|
@@ -767,6 +933,62 @@ class KubernetesClientBasedFetcher {
|
|
|
767
933
|
}
|
|
768
934
|
}
|
|
769
935
|
|
|
936
|
+
const addResourceRoutesToRouter = (router, catalogApi, objectsProvider) => {
|
|
937
|
+
const getEntityByReq = async (req) => {
|
|
938
|
+
const rawEntityRef = req.body.entityRef;
|
|
939
|
+
if (rawEntityRef && typeof rawEntityRef !== "string") {
|
|
940
|
+
throw new errors.InputError(`entity query must be a string`);
|
|
941
|
+
} else if (!rawEntityRef) {
|
|
942
|
+
throw new errors.InputError("entity is a required field");
|
|
943
|
+
}
|
|
944
|
+
let entityRef = void 0;
|
|
945
|
+
try {
|
|
946
|
+
entityRef = catalogModel.parseEntityRef(rawEntityRef);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
throw new errors.InputError(`Invalid entity ref, ${error}`);
|
|
949
|
+
}
|
|
950
|
+
const token = pluginAuthNode.getBearerTokenFromAuthorizationHeader(
|
|
951
|
+
req.headers.authorization
|
|
952
|
+
);
|
|
953
|
+
if (!token) {
|
|
954
|
+
throw new errors.AuthenticationError("No Backstage token");
|
|
955
|
+
}
|
|
956
|
+
const entity = await catalogApi.getEntityByRef(entityRef, {
|
|
957
|
+
token
|
|
958
|
+
});
|
|
959
|
+
if (!entity) {
|
|
960
|
+
throw new errors.InputError(
|
|
961
|
+
`Entity ref missing, ${catalogModel.stringifyEntityRef(entityRef)}`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
return entity;
|
|
965
|
+
};
|
|
966
|
+
router.post("/resources/workloads/query", async (req, res) => {
|
|
967
|
+
const entity = await getEntityByReq(req);
|
|
968
|
+
const response = await objectsProvider.getKubernetesObjectsByEntity({
|
|
969
|
+
entity,
|
|
970
|
+
auth: req.body.auth
|
|
971
|
+
});
|
|
972
|
+
res.json(response);
|
|
973
|
+
});
|
|
974
|
+
router.post("/resources/custom/query", async (req, res) => {
|
|
975
|
+
const entity = await getEntityByReq(req);
|
|
976
|
+
if (!req.body.customResources) {
|
|
977
|
+
throw new errors.InputError("customResources is a required field");
|
|
978
|
+
} else if (!Array.isArray(req.body.customResources)) {
|
|
979
|
+
throw new errors.InputError("customResources must be an array");
|
|
980
|
+
} else if (req.body.customResources.length === 0) {
|
|
981
|
+
throw new errors.InputError("at least 1 customResource is required");
|
|
982
|
+
}
|
|
983
|
+
const response = await objectsProvider.getCustomResourcesByEntity({
|
|
984
|
+
entity,
|
|
985
|
+
customResources: req.body.customResources,
|
|
986
|
+
auth: req.body.auth
|
|
987
|
+
});
|
|
988
|
+
res.json(response);
|
|
989
|
+
});
|
|
990
|
+
};
|
|
991
|
+
|
|
770
992
|
class KubernetesBuilder {
|
|
771
993
|
constructor(env) {
|
|
772
994
|
this.env = env;
|
|
@@ -786,7 +1008,9 @@ class KubernetesBuilder {
|
|
|
786
1008
|
if (process.env.NODE_ENV !== "development") {
|
|
787
1009
|
throw new Error("Kubernetes configuration is missing");
|
|
788
1010
|
}
|
|
789
|
-
logger.warn(
|
|
1011
|
+
logger.warn(
|
|
1012
|
+
"Failed to initialize kubernetes backend: kubernetes config is missing"
|
|
1013
|
+
);
|
|
790
1014
|
return {
|
|
791
1015
|
router: Router__default["default"]()
|
|
792
1016
|
};
|
|
@@ -802,7 +1026,11 @@ class KubernetesBuilder {
|
|
|
802
1026
|
customResources,
|
|
803
1027
|
objectTypesToFetch: this.getObjectTypesToFetch()
|
|
804
1028
|
});
|
|
805
|
-
const router = this.buildRouter(
|
|
1029
|
+
const router = this.buildRouter(
|
|
1030
|
+
objectsProvider,
|
|
1031
|
+
clusterSupplier,
|
|
1032
|
+
this.env.catalogApi
|
|
1033
|
+
);
|
|
806
1034
|
return {
|
|
807
1035
|
clusterSupplier,
|
|
808
1036
|
customResources,
|
|
@@ -834,18 +1062,26 @@ class KubernetesBuilder {
|
|
|
834
1062
|
}
|
|
835
1063
|
buildCustomResources() {
|
|
836
1064
|
var _a;
|
|
837
|
-
const customResources = ((_a = this.env.config.getOptionalConfigArray("kubernetes.customResources")) != null ? _a : []).map(
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1065
|
+
const customResources = ((_a = this.env.config.getOptionalConfigArray("kubernetes.customResources")) != null ? _a : []).map(
|
|
1066
|
+
(c) => ({
|
|
1067
|
+
group: c.getString("group"),
|
|
1068
|
+
apiVersion: c.getString("apiVersion"),
|
|
1069
|
+
plural: c.getString("plural"),
|
|
1070
|
+
objectType: "customresources"
|
|
1071
|
+
})
|
|
1072
|
+
);
|
|
1073
|
+
this.env.logger.info(
|
|
1074
|
+
`action=LoadingCustomResources numOfCustomResources=${customResources.length}`
|
|
1075
|
+
);
|
|
844
1076
|
return customResources;
|
|
845
1077
|
}
|
|
846
1078
|
buildClusterSupplier(refreshInterval) {
|
|
847
1079
|
const config = this.env.config;
|
|
848
|
-
return getCombinedClusterSupplier(
|
|
1080
|
+
return getCombinedClusterSupplier(
|
|
1081
|
+
config,
|
|
1082
|
+
this.env.catalogApi,
|
|
1083
|
+
refreshInterval
|
|
1084
|
+
);
|
|
849
1085
|
}
|
|
850
1086
|
buildObjectsProvider(options) {
|
|
851
1087
|
return new KubernetesFanOutHandler(options);
|
|
@@ -863,7 +1099,9 @@ class KubernetesBuilder {
|
|
|
863
1099
|
case "http":
|
|
864
1100
|
return this.buildHttpServiceLocator(clusterSupplier);
|
|
865
1101
|
default:
|
|
866
|
-
throw new Error(
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
`Unsupported kubernetes.clusterLocatorMethod "${method}"`
|
|
1104
|
+
);
|
|
867
1105
|
}
|
|
868
1106
|
}
|
|
869
1107
|
buildMultiTenantServiceLocator(clusterSupplier) {
|
|
@@ -872,7 +1110,7 @@ class KubernetesBuilder {
|
|
|
872
1110
|
buildHttpServiceLocator(_clusterSupplier) {
|
|
873
1111
|
throw new Error("not implemented");
|
|
874
1112
|
}
|
|
875
|
-
buildRouter(objectsProvider, clusterSupplier) {
|
|
1113
|
+
buildRouter(objectsProvider, clusterSupplier, catalogApi) {
|
|
876
1114
|
const logger = this.env.logger;
|
|
877
1115
|
const router = Router__default["default"]();
|
|
878
1116
|
router.use(express__default["default"].json());
|
|
@@ -886,7 +1124,9 @@ class KubernetesBuilder {
|
|
|
886
1124
|
});
|
|
887
1125
|
res.json(response);
|
|
888
1126
|
} catch (e) {
|
|
889
|
-
logger.error(
|
|
1127
|
+
logger.error(
|
|
1128
|
+
`action=retrieveObjectsByServiceId service=${serviceId}, error=${e}`
|
|
1129
|
+
);
|
|
890
1130
|
res.status(500).json({ error: e.message });
|
|
891
1131
|
}
|
|
892
1132
|
});
|
|
@@ -901,22 +1141,33 @@ class KubernetesBuilder {
|
|
|
901
1141
|
}))
|
|
902
1142
|
});
|
|
903
1143
|
});
|
|
1144
|
+
addResourceRoutesToRouter(router, catalogApi, objectsProvider);
|
|
904
1145
|
return router;
|
|
905
1146
|
}
|
|
906
1147
|
async fetchClusterDetails(clusterSupplier) {
|
|
907
1148
|
const clusterDetails = await clusterSupplier.getClusters();
|
|
908
|
-
this.env.logger.info(
|
|
1149
|
+
this.env.logger.info(
|
|
1150
|
+
`action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`
|
|
1151
|
+
);
|
|
909
1152
|
return clusterDetails;
|
|
910
1153
|
}
|
|
911
1154
|
getServiceLocatorMethod() {
|
|
912
|
-
return this.env.config.getString(
|
|
1155
|
+
return this.env.config.getString(
|
|
1156
|
+
"kubernetes.serviceLocatorMethod.type"
|
|
1157
|
+
);
|
|
913
1158
|
}
|
|
914
1159
|
getObjectTypesToFetch() {
|
|
915
|
-
const objectTypesToFetchStrings = this.env.config.getOptionalStringArray(
|
|
916
|
-
|
|
1160
|
+
const objectTypesToFetchStrings = this.env.config.getOptionalStringArray(
|
|
1161
|
+
"kubernetes.objectTypes"
|
|
1162
|
+
);
|
|
1163
|
+
const apiVersionOverrides = this.env.config.getOptionalConfig(
|
|
1164
|
+
"kubernetes.apiVersionOverrides"
|
|
1165
|
+
);
|
|
917
1166
|
let objectTypesToFetch;
|
|
918
1167
|
if (objectTypesToFetchStrings) {
|
|
919
|
-
objectTypesToFetch = DEFAULT_OBJECTS.filter(
|
|
1168
|
+
objectTypesToFetch = DEFAULT_OBJECTS.filter(
|
|
1169
|
+
(obj) => objectTypesToFetchStrings.includes(obj.objectType)
|
|
1170
|
+
);
|
|
920
1171
|
}
|
|
921
1172
|
if (apiVersionOverrides) {
|
|
922
1173
|
objectTypesToFetch = objectTypesToFetch != null ? objectTypesToFetch : DEFAULT_OBJECTS;
|