@backstage/plugin-kubernetes-backend 0.14.1 → 0.14.2-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 +35 -0
- package/alpha/package.json +1 -1
- package/config.d.ts +9 -4
- package/dist/index.cjs.js +92 -32
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +139 -130
- package/package.json +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @backstage/plugin-kubernetes-backend
|
|
2
2
|
|
|
3
|
+
## 0.14.2-next.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7233f57: Fixed an issue where a misleading error message would be logged when an
|
|
8
|
+
unsupported service locator method was specified.
|
|
9
|
+
- a775596: Enabled a way to include custom auth metadata info on the clusters endpoint. If you want to implement a Kubernetes auth strategy which requires surfacing custom auth metadata to the frontend, use the new presentAuthMetadata method on the AuthenticationStrategy interface.
|
|
10
|
+
- 7278d80: The purpose of this patch is to add a new login method which is `googleServiceAccount` configuring the kubernetes properties in the app-config.yaml file with authProvider key
|
|
11
|
+
- daad576: Clusters configured with the `aws` authentication strategy can now customize the
|
|
12
|
+
`x-k8s-aws-id` header value used to generate tokens. This value can be specified
|
|
13
|
+
specified via the `kubernetes.io/x-k8s-aws-id` parameter (in
|
|
14
|
+
`metadata.annotations` for clusters in the catalog, or the `authMetadata` block
|
|
15
|
+
on clusters in the app-config). This is particularly helpful when a Backstage
|
|
16
|
+
instance contains multiple AWS clusters with the same name in different regions
|
|
17
|
+
-- using this new parameter, the clusters can be given different logical names
|
|
18
|
+
to distinguish them but still use the same ID for the purposes of generating
|
|
19
|
+
tokens.
|
|
20
|
+
- f180cba: Enabling authentication to kubernetes clusters with mTLS x509 client certs
|
|
21
|
+
- 7f6ff25: Custom per-cluster auth metadata (mainly for use with custom `AuthenticationStrategy` implementations) can now be specified in the `authMetadata` property of clusters in the app-config.
|
|
22
|
+
- Updated dependencies
|
|
23
|
+
- @backstage/backend-common@0.21.0-next.0
|
|
24
|
+
- @backstage/plugin-kubernetes-node@0.1.4-next.0
|
|
25
|
+
- @backstage/catalog-client@1.6.0-next.0
|
|
26
|
+
- @backstage/plugin-kubernetes-common@0.7.4-next.0
|
|
27
|
+
- @backstage/plugin-auth-node@0.4.4-next.0
|
|
28
|
+
- @backstage/plugin-catalog-node@1.6.2-next.0
|
|
29
|
+
- @backstage/plugin-permission-node@0.7.21-next.0
|
|
30
|
+
- @backstage/backend-plugin-api@0.6.10-next.0
|
|
31
|
+
- @backstage/catalog-model@1.4.3
|
|
32
|
+
- @backstage/config@1.1.1
|
|
33
|
+
- @backstage/errors@1.2.3
|
|
34
|
+
- @backstage/integration-aws-node@0.1.8
|
|
35
|
+
- @backstage/types@1.1.1
|
|
36
|
+
- @backstage/plugin-permission-common@0.7.12
|
|
37
|
+
|
|
3
38
|
## 0.14.1
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/alpha/package.json
CHANGED
package/config.d.ts
CHANGED
|
@@ -46,13 +46,16 @@ export interface Config {
|
|
|
46
46
|
/** @visibility secret */
|
|
47
47
|
serviceAccountToken?: string;
|
|
48
48
|
/** @visibility frontend */
|
|
49
|
-
authProvider
|
|
49
|
+
authProvider?:
|
|
50
|
+
| 'aks'
|
|
50
51
|
| 'aws'
|
|
51
|
-
| 'google'
|
|
52
|
-
| 'serviceAccount'
|
|
53
52
|
| 'azure'
|
|
53
|
+
| 'google'
|
|
54
|
+
| 'googleServiceAccount'
|
|
54
55
|
| 'oidc'
|
|
55
|
-
| '
|
|
56
|
+
| 'serviceAccount';
|
|
57
|
+
/** @visibility secret */
|
|
58
|
+
authMetadata?: object;
|
|
56
59
|
/** @visibility frontend */
|
|
57
60
|
oidcTokenProvider?: string;
|
|
58
61
|
/** @visibility frontend */
|
|
@@ -81,6 +84,8 @@ export interface Config {
|
|
|
81
84
|
/** @visibility frontend */
|
|
82
85
|
region?: string;
|
|
83
86
|
/** @visibility frontend */
|
|
87
|
+
authProvider?: string;
|
|
88
|
+
/** @visibility frontend */
|
|
84
89
|
skipTLSVerify?: boolean;
|
|
85
90
|
/** @visibility frontend */
|
|
86
91
|
skipMetricsLookup?: boolean;
|
package/dist/index.cjs.js
CHANGED
|
@@ -61,6 +61,9 @@ class AksStrategy {
|
|
|
61
61
|
validateCluster() {
|
|
62
62
|
return [];
|
|
63
63
|
}
|
|
64
|
+
presentAuthMetadata(_authMetadata) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
class AnonymousStrategy {
|
|
@@ -70,6 +73,9 @@ class AnonymousStrategy {
|
|
|
70
73
|
validateCluster() {
|
|
71
74
|
return [];
|
|
72
75
|
}
|
|
76
|
+
presentAuthMetadata(_authMetadata) {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
var __defProp$b = Object.defineProperty;
|
|
@@ -85,10 +91,11 @@ class AwsIamStrategy {
|
|
|
85
91
|
this.credsManager = integrationAwsNode.DefaultAwsCredentialsManager.fromConfig(opts.config);
|
|
86
92
|
}
|
|
87
93
|
async getCredential(clusterDetails) {
|
|
94
|
+
var _a;
|
|
88
95
|
return {
|
|
89
96
|
type: "bearer token",
|
|
90
97
|
token: await this.getBearerToken(
|
|
91
|
-
clusterDetails.name,
|
|
98
|
+
(_a = clusterDetails.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AWS_CLUSTER_ID]) != null ? _a : clusterDetails.name,
|
|
92
99
|
clusterDetails.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE],
|
|
93
100
|
clusterDetails.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]
|
|
94
101
|
)
|
|
@@ -97,7 +104,7 @@ class AwsIamStrategy {
|
|
|
97
104
|
validateCluster() {
|
|
98
105
|
return [];
|
|
99
106
|
}
|
|
100
|
-
async getBearerToken(
|
|
107
|
+
async getBearerToken(clusterId, assumeRole, externalId) {
|
|
101
108
|
var _a, _b;
|
|
102
109
|
const region = (_a = process.env.AWS_REGION) != null ? _a : defaultRegion;
|
|
103
110
|
let credentials = (await this.credsManager.getCredentialProvider()).sdkCredentialProvider;
|
|
@@ -123,7 +130,7 @@ class AwsIamStrategy {
|
|
|
123
130
|
{
|
|
124
131
|
headers: {
|
|
125
132
|
host: `sts.${region}.amazonaws.com`,
|
|
126
|
-
"x-k8s-aws-id":
|
|
133
|
+
"x-k8s-aws-id": clusterId
|
|
127
134
|
},
|
|
128
135
|
hostname: `sts.${region}.amazonaws.com`,
|
|
129
136
|
method: "GET",
|
|
@@ -147,6 +154,9 @@ class AwsIamStrategy {
|
|
|
147
154
|
const url = `https://${request.hostname}${request.path}?${query}`;
|
|
148
155
|
return `k8s-aws-v1.${Buffer.from(url).toString("base64url")}`;
|
|
149
156
|
}
|
|
157
|
+
presentAuthMetadata(_authMetadata) {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
var __defProp$a = Object.defineProperty;
|
|
@@ -202,6 +212,9 @@ class AzureIdentityStrategy {
|
|
|
202
212
|
tokenExpired() {
|
|
203
213
|
return Date.now() >= this.accessToken.expiresOnTimestamp;
|
|
204
214
|
}
|
|
215
|
+
presentAuthMetadata(_authMetadata) {
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
205
218
|
}
|
|
206
219
|
|
|
207
220
|
class GoogleStrategy {
|
|
@@ -217,6 +230,9 @@ class GoogleStrategy {
|
|
|
217
230
|
validateCluster() {
|
|
218
231
|
return [];
|
|
219
232
|
}
|
|
233
|
+
presentAuthMetadata(_authMetadata) {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
220
236
|
}
|
|
221
237
|
|
|
222
238
|
class GoogleServiceAccountStrategy {
|
|
@@ -233,6 +249,9 @@ class GoogleServiceAccountStrategy {
|
|
|
233
249
|
validateCluster() {
|
|
234
250
|
return [];
|
|
235
251
|
}
|
|
252
|
+
presentAuthMetadata(_authMetadata) {
|
|
253
|
+
return {};
|
|
254
|
+
}
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
var __defProp$9 = Object.defineProperty;
|
|
@@ -267,6 +286,9 @@ class DispatchStrategy {
|
|
|
267
286
|
}
|
|
268
287
|
return strategy.validateCluster(authMetadata);
|
|
269
288
|
}
|
|
289
|
+
presentAuthMetadata(_authMetadata) {
|
|
290
|
+
return {};
|
|
291
|
+
}
|
|
270
292
|
}
|
|
271
293
|
|
|
272
294
|
class ServiceAccountStrategy {
|
|
@@ -286,6 +308,9 @@ class ServiceAccountStrategy {
|
|
|
286
308
|
validateCluster() {
|
|
287
309
|
return [];
|
|
288
310
|
}
|
|
311
|
+
presentAuthMetadata(_authMetadata) {
|
|
312
|
+
return {};
|
|
313
|
+
}
|
|
289
314
|
}
|
|
290
315
|
|
|
291
316
|
class OidcStrategy {
|
|
@@ -312,6 +337,9 @@ class OidcStrategy {
|
|
|
312
337
|
}
|
|
313
338
|
return [];
|
|
314
339
|
}
|
|
340
|
+
presentAuthMetadata(_authMetadata) {
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
315
343
|
}
|
|
316
344
|
|
|
317
345
|
var __defProp$8 = Object.defineProperty;
|
|
@@ -328,18 +356,26 @@ class ConfigClusterLocator {
|
|
|
328
356
|
static fromConfig(config, authStrategy) {
|
|
329
357
|
return new ConfigClusterLocator(
|
|
330
358
|
config.getConfigArray("clusters").map((c) => {
|
|
331
|
-
var _a, _b;
|
|
332
|
-
const
|
|
359
|
+
var _a, _b, _c;
|
|
360
|
+
const authMetadataBlock = c.getOptional("authMetadata");
|
|
361
|
+
const name = c.getString("name");
|
|
362
|
+
const authProvider = (_a = authMetadataBlock == null ? void 0 : authMetadataBlock[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER]) != null ? _a : c.getOptionalString("authProvider");
|
|
363
|
+
if (!authProvider) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`cluster '${name}' has no auth provider configured; this must be specified via the 'authProvider' or 'authMetadata.${pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER}' parameter`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
333
368
|
const clusterDetails = {
|
|
334
|
-
name
|
|
369
|
+
name,
|
|
335
370
|
url: c.getString("url"),
|
|
336
|
-
skipTLSVerify: (
|
|
337
|
-
skipMetricsLookup: (
|
|
371
|
+
skipTLSVerify: (_b = c.getOptionalBoolean("skipTLSVerify")) != null ? _b : false,
|
|
372
|
+
skipMetricsLookup: (_c = c.getOptionalBoolean("skipMetricsLookup")) != null ? _c : false,
|
|
338
373
|
caData: c.getOptionalString("caData"),
|
|
339
374
|
caFile: c.getOptionalString("caFile"),
|
|
340
375
|
authMetadata: {
|
|
341
376
|
[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER]: authProvider,
|
|
342
|
-
...ConfigClusterLocator.parseAuthMetadata(c)
|
|
377
|
+
...ConfigClusterLocator.parseAuthMetadata(c),
|
|
378
|
+
...authMetadataBlock
|
|
343
379
|
}
|
|
344
380
|
};
|
|
345
381
|
const customResources = c.getOptionalConfigArray("customResources");
|
|
@@ -437,8 +473,10 @@ class GkeClusterLocator {
|
|
|
437
473
|
const matchingResourceLabels = (_b = (_a = config.getOptionalConfigArray("matchingResourceLabels")) == null ? void 0 : _a.map((mrl) => {
|
|
438
474
|
return { key: mrl.getString("key"), value: mrl.getString("value") };
|
|
439
475
|
})) != null ? _b : [];
|
|
476
|
+
const storeAuthProviderString = config.getOptionalString("authProvider") === "googleServiceAccount" ? "googleServiceAccount" : "google";
|
|
440
477
|
const options = {
|
|
441
478
|
projectId: config.getString("projectId"),
|
|
479
|
+
authProvider: storeAuthProviderString,
|
|
442
480
|
region: (_c = config.getOptionalString("region")) != null ? _c : "-",
|
|
443
481
|
skipTLSVerify: (_d = config.getOptionalBoolean("skipTLSVerify")) != null ? _d : false,
|
|
444
482
|
skipMetricsLookup: (_e = config.getOptionalBoolean("skipMetricsLookup")) != null ? _e : false,
|
|
@@ -474,6 +512,7 @@ class GkeClusterLocator {
|
|
|
474
512
|
const {
|
|
475
513
|
projectId,
|
|
476
514
|
region,
|
|
515
|
+
authProvider,
|
|
477
516
|
skipTLSVerify,
|
|
478
517
|
skipMetricsLookup,
|
|
479
518
|
exposeDashboard,
|
|
@@ -497,7 +536,7 @@ class GkeClusterLocator {
|
|
|
497
536
|
// TODO filter out clusters which don't have name or endpoint
|
|
498
537
|
name: (_a2 = r.name) != null ? _a2 : "unknown",
|
|
499
538
|
url: `https://${(_b = r.endpoint) != null ? _b : ""}`,
|
|
500
|
-
authMetadata: { [pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER]:
|
|
539
|
+
authMetadata: { [pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER]: authProvider },
|
|
501
540
|
skipTLSVerify,
|
|
502
541
|
skipMetricsLookup,
|
|
503
542
|
...exposeDashboard ? {
|
|
@@ -1133,14 +1172,14 @@ class KubernetesClientBasedFetcher {
|
|
|
1133
1172
|
let url;
|
|
1134
1173
|
let requestInit;
|
|
1135
1174
|
const authProvider = clusterDetails.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER];
|
|
1136
|
-
if (authProvider
|
|
1175
|
+
if (this.isServiceAccountAuthentication(authProvider, clusterDetails)) {
|
|
1137
1176
|
[url, requestInit] = this.fetchArgsInCluster(credential);
|
|
1138
|
-
} else if (
|
|
1177
|
+
} else if (!this.isCredentialMissing(authProvider, credential)) {
|
|
1139
1178
|
[url, requestInit] = this.fetchArgs(clusterDetails, credential);
|
|
1140
1179
|
} else {
|
|
1141
1180
|
return Promise.reject(
|
|
1142
1181
|
new Error(
|
|
1143
|
-
`no bearer token for cluster '${clusterDetails.name}' and not running in Kubernetes`
|
|
1182
|
+
`no bearer token or client cert for cluster '${clusterDetails.name}' and not running in Kubernetes`
|
|
1144
1183
|
)
|
|
1145
1184
|
);
|
|
1146
1185
|
}
|
|
@@ -1154,6 +1193,12 @@ class KubernetesClientBasedFetcher {
|
|
|
1154
1193
|
}
|
|
1155
1194
|
return fetch__default["default"](url, requestInit);
|
|
1156
1195
|
}
|
|
1196
|
+
isServiceAccountAuthentication(authProvider, clusterDetails) {
|
|
1197
|
+
return authProvider === "serviceAccount" && !clusterDetails.authMetadata.serviceAccountToken && fs__default["default"].pathExistsSync(clientNode.Config.SERVICEACCOUNT_CA_PATH);
|
|
1198
|
+
}
|
|
1199
|
+
isCredentialMissing(authProvider, credential) {
|
|
1200
|
+
return authProvider !== "localKubectlProxy" && credential.type === "anonymous";
|
|
1201
|
+
}
|
|
1157
1202
|
fetchArgs(clusterDetails, credential) {
|
|
1158
1203
|
var _a;
|
|
1159
1204
|
const requestInit = {
|
|
@@ -1173,7 +1218,11 @@ class KubernetesClientBasedFetcher {
|
|
|
1173
1218
|
clusterDetails.caFile,
|
|
1174
1219
|
clusterDetails.caData
|
|
1175
1220
|
)) != null ? _a : void 0,
|
|
1176
|
-
rejectUnauthorized: !clusterDetails.skipTLSVerify
|
|
1221
|
+
rejectUnauthorized: !clusterDetails.skipTLSVerify,
|
|
1222
|
+
...credential.type === "x509 client certificate" && {
|
|
1223
|
+
cert: credential.cert,
|
|
1224
|
+
key: credential.key
|
|
1225
|
+
}
|
|
1177
1226
|
});
|
|
1178
1227
|
}
|
|
1179
1228
|
return [url, requestInit];
|
|
@@ -1237,20 +1286,6 @@ class KubernetesProxy {
|
|
|
1237
1286
|
res.status(403).json({ error: new errors.NotAllowedError("Unauthorized") });
|
|
1238
1287
|
return;
|
|
1239
1288
|
}
|
|
1240
|
-
const authHeader = req.header(HEADER_KUBERNETES_AUTH);
|
|
1241
|
-
if (authHeader) {
|
|
1242
|
-
req.headers.authorization = authHeader;
|
|
1243
|
-
} else {
|
|
1244
|
-
const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
|
|
1245
|
-
req.headers
|
|
1246
|
-
);
|
|
1247
|
-
const credential = await this.getClusterForRequest(req).then((cd) => {
|
|
1248
|
-
return this.authStrategy.getCredential(cd, authObj);
|
|
1249
|
-
});
|
|
1250
|
-
if (credential.type === "bearer token") {
|
|
1251
|
-
req.headers.authorization = `Bearer ${credential.token}`;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
1289
|
const middleware = await this.getMiddleware(req);
|
|
1255
1290
|
if (((_a = req.header("connection")) == null ? void 0 : _a.toLowerCase()) === "upgrade" && ((_b = req.header("upgrade")) == null ? void 0 : _b.toLowerCase()) === "websocket") {
|
|
1256
1291
|
middleware.upgrade(req, req.socket, void 0);
|
|
@@ -1284,7 +1319,7 @@ class KubernetesProxy {
|
|
|
1284
1319
|
var _a;
|
|
1285
1320
|
const cluster = await this.getClusterForRequest(req);
|
|
1286
1321
|
const url = new URL(cluster.url);
|
|
1287
|
-
|
|
1322
|
+
const target = {
|
|
1288
1323
|
protocol: url.protocol,
|
|
1289
1324
|
host: url.hostname,
|
|
1290
1325
|
port: url.port,
|
|
@@ -1293,6 +1328,24 @@ class KubernetesProxy {
|
|
|
1293
1328
|
cluster.caData
|
|
1294
1329
|
)) == null ? void 0 : _a.toString()
|
|
1295
1330
|
};
|
|
1331
|
+
const authHeader = req.header(HEADER_KUBERNETES_AUTH);
|
|
1332
|
+
if (authHeader) {
|
|
1333
|
+
req.headers.authorization = authHeader;
|
|
1334
|
+
} else {
|
|
1335
|
+
const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
|
|
1336
|
+
req.headers
|
|
1337
|
+
);
|
|
1338
|
+
const credential = await this.getClusterForRequest(req).then((cd) => {
|
|
1339
|
+
return this.authStrategy.getCredential(cd, authObj);
|
|
1340
|
+
});
|
|
1341
|
+
if (credential.type === "bearer token") {
|
|
1342
|
+
req.headers.authorization = `Bearer ${credential.token}`;
|
|
1343
|
+
} else if (credential.type === "x509 client certificate") {
|
|
1344
|
+
target.key = credential.key;
|
|
1345
|
+
target.cert = credential.cert;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return target;
|
|
1296
1349
|
},
|
|
1297
1350
|
onError: (error, req, res) => {
|
|
1298
1351
|
const wrappedError = new errors.ForwardedError(
|
|
@@ -1523,7 +1576,7 @@ class KubernetesBuilder {
|
|
|
1523
1576
|
break;
|
|
1524
1577
|
default:
|
|
1525
1578
|
throw new Error(
|
|
1526
|
-
`Unsupported kubernetes.
|
|
1579
|
+
`Unsupported kubernetes.serviceLocatorMethod "${method}"`
|
|
1527
1580
|
);
|
|
1528
1581
|
}
|
|
1529
1582
|
return this.serviceLocator;
|
|
@@ -1580,11 +1633,18 @@ class KubernetesBuilder {
|
|
|
1580
1633
|
res.json({
|
|
1581
1634
|
items: clusterDetails.map((cd) => {
|
|
1582
1635
|
const oidcTokenProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
|
|
1636
|
+
const authProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER];
|
|
1637
|
+
const strategy = this.getAuthStrategyMap()[authProvider];
|
|
1638
|
+
let auth = {};
|
|
1639
|
+
if (strategy) {
|
|
1640
|
+
auth = strategy.presentAuthMetadata(cd.authMetadata);
|
|
1641
|
+
}
|
|
1583
1642
|
return {
|
|
1584
1643
|
name: cd.name,
|
|
1585
1644
|
dashboardUrl: cd.dashboardUrl,
|
|
1586
|
-
authProvider
|
|
1587
|
-
...oidcTokenProvider && { oidcTokenProvider }
|
|
1645
|
+
authProvider,
|
|
1646
|
+
...oidcTokenProvider && { oidcTokenProvider },
|
|
1647
|
+
...auth && Object.keys(auth).length !== 0 && { auth }
|
|
1588
1648
|
};
|
|
1589
1649
|
})
|
|
1590
1650
|
});
|