Kubernetes: lookup cluster config from google api when using GKE (#4844)
* Restructure config; add GKE cluster locator Signed-off-by: mclarke <mclarke@spotify.com> * PR feedback Signed-off-by: mclarke <mclarke@spotify.com> * fix region typo Signed-off-by: mclarke <mclarke@spotify.com> * replace config api call with clusters endpoint Signed-off-by: mclarke <mclarke@spotify.com> * missed tsc error Signed-off-by: mclarke <mclarke@spotify.com> * doc tweak Signed-off-by: mclarke <mclarke@spotify.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': minor
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
Restructure configuration; Add GKE cluster locator
|
||||
|
||||
Config migration
|
||||
|
||||
1. `kubernetes.clusters` is now at `kubernetes.clusterLocatorMethods[].clusters` when the `clusterLocatorMethod` is of `type: 'config''`
|
||||
2. `kubernetes.serviceLocatorMethod` is now an object. `multiTenant` is the only valid `type` currently
|
||||
|
||||
Old config example:
|
||||
|
||||
```yaml
|
||||
kubernetes:
|
||||
serviceLocatorMethod: 'multiTenant'
|
||||
clusterLocatorMethods:
|
||||
- 'config'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
authProvider: 'serviceAccount'
|
||||
serviceAccountToken:
|
||||
$env: K8S_MINIKUBE_TOKEN
|
||||
- url: http://127.0.0.2:9999
|
||||
name: aws-cluster-1
|
||||
authProvider: 'aws'
|
||||
```
|
||||
|
||||
New config example:
|
||||
|
||||
```yaml
|
||||
kubernetes:
|
||||
serviceLocatorMethod:
|
||||
type: 'multiTenant'
|
||||
clusterLocatorMethods:
|
||||
- type: 'config'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
authProvider: 'serviceAccount'
|
||||
serviceAccountToken:
|
||||
$env: K8S_MINIKUBE_TOKEN
|
||||
- url: http://127.0.0.2:9999
|
||||
name: aws-cluster-1
|
||||
authProvider: 'aws'
|
||||
```
|
||||
+4
-3
@@ -108,10 +108,11 @@ lighthouse:
|
||||
baseUrl: http://localhost:3003
|
||||
|
||||
kubernetes:
|
||||
serviceLocatorMethod: 'multiTenant'
|
||||
serviceLocatorMethod:
|
||||
type: 'multiTenant'
|
||||
clusterLocatorMethods:
|
||||
- 'config'
|
||||
clusters: []
|
||||
- type: 'config'
|
||||
clusters: []
|
||||
|
||||
kafka:
|
||||
clientId: backstage
|
||||
|
||||
@@ -17,18 +17,22 @@ The following is a full example entry in `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
kubernetes:
|
||||
serviceLocatorMethod: 'multiTenant'
|
||||
serviceLocatorMethod:
|
||||
type: 'multiTenant'
|
||||
clusterLocatorMethods:
|
||||
- 'config'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
authProvider: 'serviceAccount'
|
||||
serviceAccountToken:
|
||||
$env: K8S_MINIKUBE_TOKEN
|
||||
- url: http://127.0.0.2:9999
|
||||
name: gke-cluster-1
|
||||
authProvider: 'google'
|
||||
- type: 'config'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
authProvider: 'serviceAccount'
|
||||
serviceAccountToken:
|
||||
$env: K8S_MINIKUBE_TOKEN
|
||||
- url: http://127.0.0.2:9999
|
||||
name: aws-cluster-1
|
||||
authProvider: 'aws'
|
||||
- type: 'gke'
|
||||
projectId: 'gke-clusters'
|
||||
region: 'europe-west1'
|
||||
```
|
||||
|
||||
### `serviceLocatorMethod`
|
||||
@@ -44,26 +48,28 @@ Currently, the only valid value is:
|
||||
|
||||
This is an array used to determine where to retrieve cluster configuration from.
|
||||
|
||||
Currently, the only valid cluster locator method is:
|
||||
Valid cluster locator methods are:
|
||||
|
||||
- `config` - This cluster locator method will read cluster information from your
|
||||
app-config (see below).
|
||||
#### `config`
|
||||
|
||||
### `clusters`
|
||||
This cluster locator method will read cluster information from your app-config
|
||||
(see below).
|
||||
|
||||
##### `clusters`
|
||||
|
||||
Used by the `config` cluster locator method to construct Kubernetes clients.
|
||||
|
||||
### `clusters.\*.url`
|
||||
##### `clusters.\*.url`
|
||||
|
||||
The base URL to the Kubernetes control plane. Can be found by using the
|
||||
"Kubernetes master" result from running the `kubectl cluster-info` command.
|
||||
|
||||
### `clusters.\*.name`
|
||||
##### `clusters.\*.name`
|
||||
|
||||
A name to represent this cluster, this must be unique within the `clusters`
|
||||
array. Users will see this value in the Service Catalog Kubernetes plugin.
|
||||
|
||||
### `clusters.\*.authProvider`
|
||||
##### `clusters.\*.authProvider`
|
||||
|
||||
This determines how the Kubernetes client authenticates with the Kubernetes
|
||||
cluster. Valid values are:
|
||||
@@ -73,7 +79,7 @@ cluster. Valid values are:
|
||||
| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
|
||||
| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
|
||||
|
||||
### `clusters.\*.serviceAccountToken` (optional)
|
||||
##### `clusters.\*.serviceAccountToken` (optional)
|
||||
|
||||
The service account token to be used when using the `serviceAccount` auth
|
||||
provider. You could get the service account token with:
|
||||
@@ -85,6 +91,38 @@ kubectl -n <NAMESPACE> get secret $(kubectl -n <NAMESPACE> get sa <SERVICE_ACCOU
|
||||
| base64 --decode
|
||||
```
|
||||
|
||||
#### `gke`
|
||||
|
||||
This cluster locator is designed to work with Kubernetes clusters running in
|
||||
[GKE][1]. It will configure the Kubernetes backend plugin to make requests to
|
||||
clusters running within a Google Cloud project.
|
||||
|
||||
This cluster locator method will use the `google` authentication mechanism.
|
||||
|
||||
The Google Cloud service account to use can be configured through the
|
||||
`GOOGLE_APPLICATION_CREDENTIALS` environment variable. Consult the [Google Cloud
|
||||
docs][2] for more information.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
- type: 'gke'
|
||||
projectId: 'gke-clusters'
|
||||
region: 'europe-west1'
|
||||
```
|
||||
|
||||
Will configure the Kubernetes plugin to connect to all GKE clusters in the
|
||||
project `gke-clusters` in the region `europe-west1`.
|
||||
|
||||
##### `projectId`
|
||||
|
||||
The Google Cloud project to look for Kubernetes clusters in.
|
||||
|
||||
##### `region` (optional)
|
||||
|
||||
The Google Cloud region to look for Kubernetes clusters in. Defaults to all
|
||||
regions.
|
||||
|
||||
### Role Based Access Control
|
||||
|
||||
The current RBAC permissions required are read-only cluster wide, for the
|
||||
@@ -135,3 +173,6 @@ for more info.
|
||||
```yaml
|
||||
'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end'
|
||||
```
|
||||
|
||||
[1]: https://cloud.google.com/kubernetes-engine
|
||||
[2]: https://cloud.google.com/docs/authentication/production#linux-or-macos
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@backstage/backend-common": "^0.5.5",
|
||||
"@backstage/catalog-model": "^0.7.3",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@google-cloud/container": "^2.2.0",
|
||||
"@kubernetes/client-node": "^0.13.2",
|
||||
"@types/express": "^4.17.6",
|
||||
"aws4": "^1.11.0",
|
||||
|
||||
+7
-8
@@ -13,15 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClusterLocatorMethod } from './src/types';
|
||||
|
||||
export interface Config {
|
||||
kubernetes?: {
|
||||
serviceLocatorMethod: 'multiTenant';
|
||||
clusterLocatorMethods: 'config'[];
|
||||
clusters: {
|
||||
url: string;
|
||||
name: string;
|
||||
serviceAccountToken: string | undefined;
|
||||
authProvider: 'aws' | 'google' | 'serviceAccount';
|
||||
}[];
|
||||
serviceLocatorMethod: {
|
||||
type: 'multiTenant';
|
||||
};
|
||||
clusterLocatorMethods: ClusterLocatorMethod[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,9 +24,7 @@ describe('ConfigClusterLocator', () => {
|
||||
clusters: [],
|
||||
});
|
||||
|
||||
const sut = ConfigClusterLocator.fromConfig(
|
||||
config.getConfigArray('clusters'),
|
||||
);
|
||||
const sut = ConfigClusterLocator.fromConfig(config);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
@@ -44,9 +42,7 @@ describe('ConfigClusterLocator', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const sut = ConfigClusterLocator.fromConfig(
|
||||
config.getConfigArray('clusters'),
|
||||
);
|
||||
const sut = ConfigClusterLocator.fromConfig(config);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
@@ -77,9 +73,7 @@ describe('ConfigClusterLocator', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const sut = ConfigClusterLocator.fromConfig(
|
||||
config.getConfigArray('clusters'),
|
||||
);
|
||||
const sut = ConfigClusterLocator.fromConfig(config);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
this.clusterDetails = clusterDetails;
|
||||
}
|
||||
|
||||
static fromConfig(config: Config[]): ConfigClusterLocator {
|
||||
static fromConfig(config: Config): ConfigClusterLocator {
|
||||
// TODO: Add validation that authProvider is required and serviceAccountToken
|
||||
// is required if authProvider is serviceAccount
|
||||
return new ConfigClusterLocator(
|
||||
config.map(c => {
|
||||
config.getConfigArray('clusters').map(c => {
|
||||
return {
|
||||
name: c.getString('name'),
|
||||
url: c.getString('url'),
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '@backstage/backend-common';
|
||||
import { ConfigReader, Config } from '@backstage/config';
|
||||
import { GkeClusterLocator } from './GkeClusterLocator';
|
||||
|
||||
const mockedListClusters = jest.fn();
|
||||
|
||||
describe('GkeClusterLocator', () => {
|
||||
beforeEach(() => {
|
||||
mockedListClusters.mockRestore();
|
||||
});
|
||||
describe('config-parsing', () => {
|
||||
it('should accept missing region', async () => {
|
||||
const config: Config = new ConfigReader({
|
||||
type: 'gke',
|
||||
projectId: 'some-project',
|
||||
});
|
||||
|
||||
GkeClusterLocator.fromConfigWithClient(config, {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
expect(mockedListClusters).toBeCalledTimes(0);
|
||||
});
|
||||
it('should not accept missing projectId', async () => {
|
||||
const config: Config = new ConfigReader({
|
||||
type: 'gke',
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
GkeClusterLocator.fromConfigWithClient(config, {
|
||||
listClusters: mockedListClusters,
|
||||
} as any),
|
||||
).toThrow("Missing required config value at 'projectId'");
|
||||
|
||||
expect(mockedListClusters).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
describe('listClusters', () => {
|
||||
it('empty clusters returns empty cluster details', async () => {
|
||||
mockedListClusters.mockReturnValueOnce([
|
||||
{
|
||||
clusters: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const config: Config = new ConfigReader({
|
||||
type: 'gke',
|
||||
projectId: 'some-project',
|
||||
region: 'some-region',
|
||||
});
|
||||
|
||||
const sut = GkeClusterLocator.fromConfigWithClient(config, {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([]);
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
expect(mockedListClusters).toHaveBeenCalledWith({
|
||||
parent: 'projects/some-project/locations/some-region',
|
||||
});
|
||||
});
|
||||
it('1 cluster returns 1 cluster details', async () => {
|
||||
mockedListClusters.mockReturnValueOnce([
|
||||
{
|
||||
clusters: [
|
||||
{
|
||||
name: 'some-cluster',
|
||||
endpoint: '1.2.3.4',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const config: Config = new ConfigReader({
|
||||
type: 'gke',
|
||||
projectId: 'some-project',
|
||||
region: 'some-region',
|
||||
});
|
||||
|
||||
const sut = GkeClusterLocator.fromConfigWithClient(config, {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
authProvider: 'google',
|
||||
name: 'some-cluster',
|
||||
url: 'https://1.2.3.4',
|
||||
},
|
||||
]);
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
expect(mockedListClusters).toHaveBeenCalledWith({
|
||||
parent: 'projects/some-project/locations/some-region',
|
||||
});
|
||||
});
|
||||
it('use region wildcard when no region provided', async () => {
|
||||
mockedListClusters.mockReturnValueOnce([
|
||||
{
|
||||
clusters: [
|
||||
{
|
||||
name: 'some-cluster',
|
||||
endpoint: '1.2.3.4',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const config: Config = new ConfigReader({
|
||||
type: 'gke',
|
||||
projectId: 'some-project',
|
||||
});
|
||||
|
||||
const sut = GkeClusterLocator.fromConfigWithClient(config, {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
authProvider: 'google',
|
||||
name: 'some-cluster',
|
||||
url: 'https://1.2.3.4',
|
||||
},
|
||||
]);
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
expect(mockedListClusters).toHaveBeenCalledWith({
|
||||
parent: 'projects/some-project/locations/-',
|
||||
});
|
||||
});
|
||||
it('2 cluster returns 2 cluster details', async () => {
|
||||
mockedListClusters.mockReturnValueOnce([
|
||||
{
|
||||
clusters: [
|
||||
{
|
||||
name: 'some-cluster',
|
||||
endpoint: '1.2.3.4',
|
||||
},
|
||||
{
|
||||
name: 'some-other-cluster',
|
||||
endpoint: '6.7.8.9',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const config: Config = new ConfigReader({
|
||||
type: 'gke',
|
||||
projectId: 'some-project',
|
||||
region: 'some-region',
|
||||
});
|
||||
|
||||
const sut = GkeClusterLocator.fromConfigWithClient(config, {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
authProvider: 'google',
|
||||
name: 'some-cluster',
|
||||
url: 'https://1.2.3.4',
|
||||
},
|
||||
{
|
||||
authProvider: 'google',
|
||||
name: 'some-other-cluster',
|
||||
url: 'https://6.7.8.9',
|
||||
},
|
||||
]);
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
expect(mockedListClusters).toHaveBeenCalledWith({
|
||||
parent: 'projects/some-project/locations/some-region',
|
||||
});
|
||||
});
|
||||
it('Handle errors gracefully', async () => {
|
||||
mockedListClusters.mockImplementation(() => {
|
||||
throw new Error('some error');
|
||||
});
|
||||
|
||||
const config: Config = new ConfigReader({
|
||||
type: 'gke',
|
||||
projectId: 'some-project',
|
||||
region: 'some-region',
|
||||
});
|
||||
|
||||
const sut = GkeClusterLocator.fromConfigWithClient(config, {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
await expect(sut.getClusters()).rejects.toThrow(
|
||||
'There was an error retrieving clusters from GKE for projectId=some-project region=some-region : [some error]',
|
||||
);
|
||||
|
||||
expect(mockedListClusters).toBeCalledTimes(1);
|
||||
expect(mockedListClusters).toHaveBeenCalledWith({
|
||||
parent: 'projects/some-project/locations/some-region',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClusterDetails, KubernetesClustersSupplier } from '..';
|
||||
import { Config } from '@backstage/config';
|
||||
import * as container from '@google-cloud/container';
|
||||
|
||||
export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
private readonly projectId: string;
|
||||
private readonly region: string | undefined;
|
||||
private readonly client: container.v1.ClusterManagerClient;
|
||||
|
||||
constructor(
|
||||
projectId: string,
|
||||
client: container.v1.ClusterManagerClient,
|
||||
region?: string,
|
||||
) {
|
||||
this.projectId = projectId;
|
||||
this.region = region;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
static fromConfigWithClient(
|
||||
config: Config,
|
||||
client: container.v1.ClusterManagerClient,
|
||||
): GkeClusterLocator {
|
||||
const projectId = config.getString('projectId');
|
||||
const region = config.getOptionalString('region');
|
||||
return new GkeClusterLocator(projectId, client, region);
|
||||
}
|
||||
|
||||
static fromConfig(config: Config): GkeClusterLocator {
|
||||
return GkeClusterLocator.fromConfigWithClient(
|
||||
config,
|
||||
new container.v1.ClusterManagerClient(),
|
||||
);
|
||||
}
|
||||
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
const region = this.region ?? '-';
|
||||
const request = {
|
||||
parent: `projects/${this.projectId}/locations/${region}`,
|
||||
};
|
||||
|
||||
try {
|
||||
const [response] = await this.client.listClusters(request);
|
||||
return (response.clusters ?? []).map(r => ({
|
||||
// TODO filter out clusters which don't have name or endpoint
|
||||
name: r.name ?? 'unknown',
|
||||
url: `https://${r.endpoint ?? ''}`,
|
||||
authProvider: 'google',
|
||||
}));
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`There was an error retrieving clusters from GKE for projectId=${this.projectId} region=${region} : [${e.message}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,17 +22,22 @@ describe('getCombinedClusterDetails', () => {
|
||||
const config: Config = new ConfigReader(
|
||||
{
|
||||
kubernetes: {
|
||||
clusters: [
|
||||
clusterLocatorMethods: [
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
type: 'config',
|
||||
clusters: [
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -40,7 +45,7 @@ describe('getCombinedClusterDetails', () => {
|
||||
'ctx',
|
||||
);
|
||||
|
||||
const result = await getCombinedClusterDetails(['config'], config);
|
||||
const result = await getCombinedClusterDetails(config);
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
@@ -59,9 +64,36 @@ describe('getCombinedClusterDetails', () => {
|
||||
});
|
||||
|
||||
it('throws an error when using an unsupported cluster locator', async () => {
|
||||
await expect(
|
||||
getCombinedClusterDetails(['magic' as any], new ConfigReader({})),
|
||||
).rejects.toStrictEqual(
|
||||
const config: Config = new ConfigReader(
|
||||
{
|
||||
kubernetes: {
|
||||
clusterLocatorMethods: [
|
||||
{
|
||||
type: 'config',
|
||||
clusters: [
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'magic',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'ctx',
|
||||
);
|
||||
|
||||
await expect(getCombinedClusterDetails(config)).rejects.toStrictEqual(
|
||||
new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,29 +14,34 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClusterDetails, ClusterLocatorMethod } from '..';
|
||||
import { ClusterDetails } from '..';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigClusterLocator } from './ConfigClusterLocator';
|
||||
|
||||
export { ConfigClusterLocator } from './ConfigClusterLocator';
|
||||
import { GkeClusterLocator } from './GkeClusterLocator';
|
||||
|
||||
export const getCombinedClusterDetails = async (
|
||||
clusterLocatorMethods: ClusterLocatorMethod[],
|
||||
rootConfig: Config,
|
||||
): Promise<ClusterDetails[]> => {
|
||||
return Promise.all(
|
||||
clusterLocatorMethods.map(clusterLocatorMethod => {
|
||||
switch (clusterLocatorMethod) {
|
||||
case 'config':
|
||||
return ConfigClusterLocator.fromConfig(
|
||||
rootConfig.getConfigArray('kubernetes.clusters'),
|
||||
).getClusters();
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported kubernetes.clusterLocatorMethods: "${clusterLocatorMethod}"`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
rootConfig
|
||||
.getConfigArray('kubernetes.clusterLocatorMethods')
|
||||
.map(clusterLocatorMethod => {
|
||||
const type = clusterLocatorMethod.getString('type');
|
||||
switch (type) {
|
||||
case 'config':
|
||||
return ConfigClusterLocator.fromConfig(
|
||||
clusterLocatorMethod,
|
||||
).getClusters();
|
||||
case 'gke':
|
||||
return GkeClusterLocator.fromConfig(
|
||||
clusterLocatorMethod,
|
||||
).getClusters();
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported kubernetes.clusterLocatorMethods: "${type}"`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then(res => {
|
||||
return res.flat();
|
||||
|
||||
@@ -29,7 +29,19 @@ describe('router', () => {
|
||||
getKubernetesObjectsByEntity: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const router = makeRouter(getVoidLogger(), kubernetesFanOutHandler);
|
||||
const router = makeRouter(getVoidLogger(), kubernetesFanOutHandler, [
|
||||
{
|
||||
name: 'some-cluster',
|
||||
authProvider: 'serviceAccount',
|
||||
url: 'https://localhost:1234',
|
||||
serviceAccountToken: 'someToken',
|
||||
},
|
||||
{
|
||||
name: 'some-other-cluster',
|
||||
url: 'https://localhost:1235',
|
||||
authProvider: 'google',
|
||||
},
|
||||
]);
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
@@ -37,6 +49,25 @@ describe('router', () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('get /clusters', () => {
|
||||
it('happy path: lists clusters', async () => {
|
||||
const response = await request(app).get('/clusters');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toStrictEqual({
|
||||
items: [
|
||||
{
|
||||
name: 'some-cluster',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'some-other-cluster',
|
||||
authProvider: 'google',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('post /services/:serviceId', () => {
|
||||
it('happy path: lists kubernetes objects without auth in request body', async () => {
|
||||
const result = {
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
KubernetesRequestBody,
|
||||
KubernetesServiceLocator,
|
||||
ServiceLocatorMethod,
|
||||
ClusterLocatorMethod,
|
||||
ClusterDetails,
|
||||
KubernetesClustersSupplier,
|
||||
} from '..';
|
||||
@@ -43,7 +42,7 @@ const getServiceLocator = (
|
||||
clusterDetails: ClusterDetails[],
|
||||
): KubernetesServiceLocator => {
|
||||
const serviceLocatorMethod = config.getString(
|
||||
'kubernetes.serviceLocatorMethod',
|
||||
'kubernetes.serviceLocatorMethod.type',
|
||||
) as ServiceLocatorMethod;
|
||||
|
||||
switch (serviceLocatorMethod) {
|
||||
@@ -61,6 +60,7 @@ const getServiceLocator = (
|
||||
export const makeRouter = (
|
||||
logger: Logger,
|
||||
kubernetesFanOutHandler: KubernetesFanOutHandler,
|
||||
clusterDetails: ClusterDetails[],
|
||||
): express.Router => {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
@@ -81,6 +81,14 @@ export const makeRouter = (
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/clusters', async (_, res) => {
|
||||
res.json({
|
||||
items: clusterDetails.map(cd => ({
|
||||
name: cd.name,
|
||||
authProvider: cd.authProvider,
|
||||
})),
|
||||
});
|
||||
});
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -96,21 +104,18 @@ export async function createRouter(
|
||||
logger,
|
||||
});
|
||||
|
||||
const clusterLocatorMethods = options.config.getStringArray(
|
||||
'kubernetes.clusterLocatorMethods',
|
||||
) as ClusterLocatorMethod[];
|
||||
|
||||
let clusterDetails: ClusterDetails[];
|
||||
|
||||
if (options.clusterSupplier) {
|
||||
clusterDetails = await options.clusterSupplier.getClusters();
|
||||
} else {
|
||||
clusterDetails = await getCombinedClusterDetails(
|
||||
clusterLocatorMethods,
|
||||
options.config,
|
||||
);
|
||||
clusterDetails = await getCombinedClusterDetails(options.config);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`,
|
||||
);
|
||||
|
||||
const serviceLocator = getServiceLocator(options.config, clusterDetails);
|
||||
|
||||
const kubernetesFanOutHandler = new KubernetesFanOutHandler(
|
||||
@@ -119,5 +124,5 @@ export async function createRouter(
|
||||
serviceLocator,
|
||||
);
|
||||
|
||||
return makeRouter(logger, kubernetesFanOutHandler);
|
||||
return makeRouter(logger, kubernetesFanOutHandler, clusterDetails);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,49 @@ export interface KubernetesFetchError {
|
||||
resourcePath?: string;
|
||||
}
|
||||
|
||||
export interface ConfigClusterLocatorMethod {
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
type: 'config';
|
||||
clusters: {
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @visibility secret
|
||||
*/
|
||||
serviceAccountToken: string | undefined;
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
authProvider: 'aws' | 'google' | 'serviceAccount';
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface GKEClusterLocatorMethod {
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
type: 'gke';
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
projectId: string;
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
region?: string;
|
||||
}
|
||||
|
||||
export type ClusterLocatorMethod =
|
||||
| ConfigClusterLocatorMethod
|
||||
| GKEClusterLocatorMethod;
|
||||
|
||||
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
|
||||
export type ClusterLocatorMethod = 'config';
|
||||
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
|
||||
|
||||
Vendored
+10
-20
@@ -13,33 +13,23 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClusterLocatorMethod } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export interface Config {
|
||||
kubernetes?: {
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
serviceLocatorMethod: 'multiTenant';
|
||||
serviceLocatorMethod: {
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
type: 'multiTenant';
|
||||
};
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
clusterLocatorMethods: 'config'[];
|
||||
clusters: {
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @visibility secret
|
||||
*/
|
||||
serviceAccountToken: string | undefined;
|
||||
/**
|
||||
* @visibility frontend
|
||||
*/
|
||||
authProvider: 'aws' | 'google' | 'serviceAccount';
|
||||
}[];
|
||||
clusterLocatorMethods: ClusterLocatorMethod[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,44 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigApi, DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import { KubernetesApi } from './types';
|
||||
import {
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export class KubernetesBackendClient implements KubernetesApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly configApi: ConfigApi;
|
||||
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
configApi: ConfigApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.configApi = options.configApi;
|
||||
}
|
||||
|
||||
private async getRequired(
|
||||
path: string,
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
private async handleResponse(response: Response): Promise<any> {
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
let message;
|
||||
@@ -69,16 +51,40 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async postRequired(
|
||||
path: string,
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
}
|
||||
|
||||
async getObjectsByEntity(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ObjectsByEntityResponse> {
|
||||
return await this.getRequired(
|
||||
return await this.postRequired(
|
||||
`/services/${requestBody.entity.metadata.name}`,
|
||||
requestBody,
|
||||
);
|
||||
}
|
||||
|
||||
getClusters(): Config[] {
|
||||
return this.configApi.getConfigArray('kubernetes.clusters');
|
||||
async getClusters(): Promise<{ name: string; authProvider: string }[]> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return (await this.handleResponse(response)).items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import {
|
||||
KubernetesRequestBody,
|
||||
@@ -31,5 +30,5 @@ export interface KubernetesApi {
|
||||
getObjectsByEntity(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
getClusters(): Config[];
|
||||
getClusters(): Promise<{ name: string; authProvider: string }[]>;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
Content,
|
||||
Page,
|
||||
@@ -168,16 +167,14 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const clusters: Config[] = kubernetesApi.getClusters();
|
||||
const allAuthProviders: string[] = clusters.map(c =>
|
||||
c.getString('authProvider'),
|
||||
);
|
||||
const authProviders: string[] = [...new Set(allAuthProviders)];
|
||||
|
||||
const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const clusters = await kubernetesApi.getClusters();
|
||||
const authProviders: string[] = [
|
||||
...new Set(clusters.map(c => c.authProvider)),
|
||||
];
|
||||
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
|
||||
let requestBody: KubernetesRequestBody = {
|
||||
entity,
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
identityApiRef,
|
||||
googleAuthApiRef,
|
||||
createRoutableExtension,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import { KubernetesBackendClient } from './api/KubernetesBackendClient';
|
||||
import { kubernetesApiRef } from './api/types';
|
||||
@@ -41,10 +40,9 @@ export const kubernetesPlugin = createPlugin({
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, identityApi, configApi }) =>
|
||||
new KubernetesBackendClient({ discoveryApi, identityApi, configApi }),
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new KubernetesBackendClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: kubernetesAuthProvidersApiRef,
|
||||
|
||||
@@ -2450,6 +2450,13 @@
|
||||
retry-request "^4.1.1"
|
||||
teeny-request "^7.0.0"
|
||||
|
||||
"@google-cloud/container@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/@google-cloud/container/-/container-2.2.0.tgz#e97ae1cee9040b6af09cc8199ed0aa2d4ae6238e"
|
||||
integrity sha512-O9xoAGo2qwBezcpIdKcp/zvSX2jUuTRISwXp26dOS+2jeJKpC8jWYU7aRUC8DGxvkEMK9wWESMe44XEBWnHq2Q==
|
||||
dependencies:
|
||||
google-gax "^2.9.2"
|
||||
|
||||
"@google-cloud/paginator@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-0.2.0.tgz#eab2e6aa4b81df7418f6c51e2071f64dab2c2fa5"
|
||||
@@ -2970,6 +2977,23 @@
|
||||
is-promise "4.0.0"
|
||||
tslib "~2.0.1"
|
||||
|
||||
"@grpc/grpc-js@~1.2.0":
|
||||
version "1.2.10"
|
||||
resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.2.10.tgz#f316d29a45fcc324e923d593cb849d292b1ed598"
|
||||
integrity sha512-wj6GkNiorWYaPiIZ767xImmw7avMMVUweTvPFg4mJWOxz2180DKwfuxhJJZ7rpc1+7D3mX/v8vJdxTuIo71Ieg==
|
||||
dependencies:
|
||||
"@types/node" ">=12.12.47"
|
||||
google-auth-library "^6.1.1"
|
||||
semver "^6.2.0"
|
||||
|
||||
"@grpc/proto-loader@^0.5.1":
|
||||
version "0.5.6"
|
||||
resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz#1dea4b8a6412b05e2d58514d507137b63a52a98d"
|
||||
integrity sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==
|
||||
dependencies:
|
||||
lodash.camelcase "^4.3.0"
|
||||
protobufjs "^6.8.6"
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.0.4"
|
||||
resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010"
|
||||
@@ -6415,7 +6439,7 @@
|
||||
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f"
|
||||
integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==
|
||||
|
||||
"@types/long@^4.0.0":
|
||||
"@types/long@^4.0.0", "@types/long@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
||||
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
||||
@@ -6527,6 +6551,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.0.26.tgz#22a3b8a46510da8944b67bfc27df02c34a35331c"
|
||||
integrity sha512-W+fpe5s91FBGE0pEa0lnqGLL4USgpLgs4nokw16SrBBco/gQxuua7KnArSEOd5iaMqbbSHV10vUDkJYJJqpXKA==
|
||||
|
||||
"@types/node@>=12.12.47":
|
||||
version "14.14.31"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
|
||||
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
|
||||
|
||||
"@types/node@^10.1.0", "@types/node@^10.12.0":
|
||||
version "10.17.35"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56"
|
||||
@@ -6537,6 +6566,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c"
|
||||
integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA==
|
||||
|
||||
"@types/node@^13.7.0":
|
||||
version "13.13.45"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-13.13.45.tgz#e6676bcca092bae5751d015f074a234d5a82eb63"
|
||||
integrity sha512-703YTEp8AwQeapI0PTXDOj+Bs/mtdV/k9VcTP7z/de+lx6XjFMKdB+JhKnK+6PZ5za7omgZ3V6qm/dNkMj/Zow==
|
||||
|
||||
"@types/node@^13.7.2":
|
||||
version "13.13.15"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766"
|
||||
@@ -13112,7 +13146,7 @@ fast-shallow-equal@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
|
||||
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
|
||||
|
||||
fast-text-encoding@^1.0.0:
|
||||
fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
|
||||
integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==
|
||||
@@ -14203,6 +14237,38 @@ google-auth-library@^6.0.0, google-auth-library@^6.1.1:
|
||||
jws "^4.0.0"
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
google-auth-library@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.0.2.tgz#cab6fc7f94ebecc97be6133d6519d9946ccf3e9d"
|
||||
integrity sha512-vjyNZR3pDLC0u7GHLfj+Hw9tGprrJwoMwkYGqURCXYITjCrP9HprOyxVV+KekdLgATtWGuDkQG2MTh0qpUPUgg==
|
||||
dependencies:
|
||||
arrify "^2.0.0"
|
||||
base64-js "^1.3.0"
|
||||
ecdsa-sig-formatter "^1.0.11"
|
||||
fast-text-encoding "^1.0.0"
|
||||
gaxios "^4.0.0"
|
||||
gcp-metadata "^4.2.0"
|
||||
gtoken "^5.0.4"
|
||||
jws "^4.0.0"
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
google-gax@^2.9.2:
|
||||
version "2.10.3"
|
||||
resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.10.3.tgz#53278bb5cc4b654876ade774a249f0241fdb15cc"
|
||||
integrity sha512-jESs/ME9WgMzfGQKJDu9ea2mEKjznKByRL+5xb8mKfHlbUfS/LxNLNCg/35RgXwVXcNSCqkEY90z8wHxvgdd/Q==
|
||||
dependencies:
|
||||
"@grpc/grpc-js" "~1.2.0"
|
||||
"@grpc/proto-loader" "^0.5.1"
|
||||
"@types/long" "^4.0.0"
|
||||
abort-controller "^3.0.0"
|
||||
duplexify "^4.0.0"
|
||||
fast-text-encoding "^1.0.3"
|
||||
google-auth-library "^7.0.2"
|
||||
is-stream-ended "^0.1.4"
|
||||
node-fetch "^2.6.1"
|
||||
protobufjs "^6.10.2"
|
||||
retry-request "^4.0.0"
|
||||
|
||||
google-p12-pem@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz#b77fb833a2eb9f7f3c689e2e54f095276f777605"
|
||||
@@ -21310,6 +21376,25 @@ proto-list@~1.2.1:
|
||||
resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
|
||||
|
||||
protobufjs@^6.10.2, protobufjs@^6.8.6:
|
||||
version "6.10.2"
|
||||
resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz#b9cb6bd8ec8f87514592ba3fdfd28e93f33a469b"
|
||||
integrity sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==
|
||||
dependencies:
|
||||
"@protobufjs/aspromise" "^1.1.2"
|
||||
"@protobufjs/base64" "^1.1.2"
|
||||
"@protobufjs/codegen" "^2.0.4"
|
||||
"@protobufjs/eventemitter" "^1.1.0"
|
||||
"@protobufjs/fetch" "^1.1.0"
|
||||
"@protobufjs/float" "^1.0.2"
|
||||
"@protobufjs/inquire" "^1.1.0"
|
||||
"@protobufjs/path" "^1.1.2"
|
||||
"@protobufjs/pool" "^1.1.0"
|
||||
"@protobufjs/utf8" "^1.1.0"
|
||||
"@types/long" "^4.0.1"
|
||||
"@types/node" "^13.7.0"
|
||||
long "^4.0.0"
|
||||
|
||||
protocols@^1.1.0, protocols@^1.4.0:
|
||||
version "1.4.7"
|
||||
resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32"
|
||||
|
||||
Reference in New Issue
Block a user