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:
Matthew Clarke
2021-03-08 10:27:09 +00:00
committed by GitHub
parent 8e15b19a30
commit 9581ff0b4f
20 changed files with 710 additions and 142 deletions
+48
View File
@@ -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
View File
@@ -108,10 +108,11 @@ lighthouse:
baseUrl: http://localhost:3003
kubernetes:
serviceLocatorMethod: 'multiTenant'
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- 'config'
clusters: []
- type: 'config'
clusters: []
kafka:
clientId: backstage
+60 -19
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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);
}
+44 -1
View File
@@ -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';
+10 -20
View File
@@ -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;
}
}
+1 -2
View File
@@ -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,
+2 -4
View File
@@ -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,
+87 -2
View File
@@ -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"