Merge branch 'master' of https://github.com/spotify/backstage into lintMod
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
# @backstage/plugin-kubernetes-backend
|
||||
|
||||
## 0.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 681111228: Add AWS auth provider for Kubernetes
|
||||
- Updated dependencies [26a3a6cf0]
|
||||
- Updated dependencies [664dd08c9]
|
||||
- Updated dependencies [9dd057662]
|
||||
- @backstage/backend-common@0.5.1
|
||||
|
||||
## 0.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,83 +1,19 @@
|
||||
# Kubernetes Backend
|
||||
|
||||
WORK IN PROGRESS
|
||||
This is the backend part of the Kubernetes plugin for Backstage. It is called by and responds to requests from the frontend [`@backstage/plugin-kubernetes`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes) plugin.
|
||||
|
||||
This is the backend part of the Kubernetes plugin.
|
||||
It directly interfaces with the Kubernetes API control plane to obtain information about objects that will then be presented at the front end.
|
||||
|
||||
It responds to Kubernetes requests from the frontend.
|
||||
## Introduction
|
||||
|
||||
## Configuration
|
||||
See our announcement blog post [New Backstage feature: Kubernetes for Service Owners](https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners) to learn more about the motivation behind developing the plugin.
|
||||
|
||||
### serviceLocatorMethod
|
||||
## Setup & Configuration
|
||||
|
||||
This configures how to determine which clusters a component is running in.
|
||||
This plugin must be explicitly added to a Backstage app, along with it's peer frontend plugin.
|
||||
|
||||
Currently, the only valid serviceLocatorMethod is:
|
||||
The plugin requires configuration in the Backstage `app-config.yaml` to connect to a Kubernetes API control plane.
|
||||
|
||||
#### multiTenant
|
||||
In addition, configuration of an entity's `catalog-info.yaml` helps identify which specific Kubernetes object(s) should be presented on a specific entity catalog page.
|
||||
|
||||
This configuration assumes that all components run on all the provided clusters.
|
||||
|
||||
### clusterLocatorMethods
|
||||
|
||||
This is used to determine where to retrieve cluster configuration from.
|
||||
|
||||
Currently, the only valid serviceLocatorMethod is:
|
||||
|
||||
#### config
|
||||
|
||||
This clusterLocatorMethod will read cluster information in from config
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
kubernetes:
|
||||
serviceLocatorMethod: 'multiTenant'
|
||||
clusterLocatorMethods:
|
||||
- 'config'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
serviceAccountToken: <TOKEN FROM STEP 4>
|
||||
authProvider: 'serviceAccount'
|
||||
- url: http://127.0.0.2:9999
|
||||
name: gke-cluster-1
|
||||
authProvider: 'google'
|
||||
```
|
||||
|
||||
##### clusters
|
||||
|
||||
Used by the `config` `clusterLocatorMethods` to construct Kubernetes clients.
|
||||
|
||||
###### 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.
|
||||
|
||||
###### 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.
|
||||
|
||||
###### authProvider
|
||||
|
||||
This determines how the Kubernetes client authenticate with the Kubernetes cluster. Valid values are:
|
||||
|
||||
| Value | Description |
|
||||
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
|
||||
###### serviceAccount (optional)
|
||||
|
||||
The service account token to be used when using the `authProvider`, `serviceAccount`.
|
||||
|
||||
## RBAC
|
||||
|
||||
The current RBAC permissions required are read-only cluster wide, for the following objects:
|
||||
|
||||
- pods
|
||||
- services
|
||||
- configmaps
|
||||
- deployments
|
||||
- replicasets
|
||||
- horizontalpodautoscalers
|
||||
- ingresses
|
||||
For more information, see the [formal documentation about the Kubernetes feature in Backstage](https://backstage.io/docs/features/kubernetes/overview).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-kubernetes-backend",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -31,11 +31,13 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.5.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.3.0",
|
||||
"@backstage/backend-common": "^0.5.1",
|
||||
"@backstage/catalog-model": "^0.7.0",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@kubernetes/client-node": "^0.13.2",
|
||||
"@types/express": "^4.17.6",
|
||||
"aws4": "^1.11.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
@@ -49,7 +51,8 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.4.7",
|
||||
"@backstage/cli": "^0.5.0",
|
||||
"@types/aws4": "^1.5.1",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ export interface Config {
|
||||
clusters: {
|
||||
url: string;
|
||||
name: string;
|
||||
serviceAccountToken: string;
|
||||
authProvider: 'serviceAccount';
|
||||
serviceAccountToken: string | undefined;
|
||||
authProvider: 'aws' | 'google' | 'serviceAccount';
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
const mockCredentialProvider = jest.fn();
|
||||
jest.mock('@aws-sdk/credential-provider-node', () => {
|
||||
return {
|
||||
defaultProvider: () => mockCredentialProvider,
|
||||
};
|
||||
});
|
||||
|
||||
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
|
||||
|
||||
describe('AwsIamKubernetesAuthTranslator tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('returns a signed url for aws credentials', async () => {
|
||||
const authTranslator = new AwsIamKubernetesAuthTranslator();
|
||||
|
||||
mockCredentialProvider.mockImplementation(async () => {
|
||||
// These credentials are not real.
|
||||
// Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
|
||||
return {
|
||||
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secretKeyId: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
};
|
||||
});
|
||||
|
||||
const clusterDetails = await authTranslator.decorateClusterDetailsWithAuth({
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authProvider: 'aws',
|
||||
});
|
||||
expect(clusterDetails.serviceAccountToken).toBeDefined();
|
||||
});
|
||||
|
||||
it('throws when unable to get aws credentials', async () => {
|
||||
const authTranslator = new AwsIamKubernetesAuthTranslator();
|
||||
|
||||
mockCredentialProvider.mockImplementation(async () => {
|
||||
throw new Error('not implemented');
|
||||
});
|
||||
|
||||
const promise = authTranslator.decorateClusterDetailsWithAuth({
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authProvider: 'aws',
|
||||
});
|
||||
await expect(promise).rejects.toThrow('not implemented');
|
||||
});
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { defaultProvider } from '@aws-sdk/credential-provider-node';
|
||||
import { sign } from 'aws4';
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { ClusterDetails } from '..';
|
||||
|
||||
const base64 = (str: string) =>
|
||||
Buffer.from(str.toString(), 'binary').toString('base64');
|
||||
const prepend = (prep: string) => (str: string) => prep + str;
|
||||
const replace = (search: string | RegExp, substitution: string) => (
|
||||
str: string,
|
||||
) => str.replace(search, substitution);
|
||||
const pipe = (fns: ReadonlyArray<any>) => (thing: string): string =>
|
||||
fns.reduce((val, fn) => fn(val), thing);
|
||||
const removePadding = replace(/=+$/, '');
|
||||
const makeUrlSafe = pipe([replace('+', '-'), replace('/', '_')]);
|
||||
|
||||
export class AwsIamKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator {
|
||||
async getBearerToken(clusterName: string): Promise<string> {
|
||||
const credentialProvider = defaultProvider();
|
||||
const credentials = await credentialProvider();
|
||||
const request = {
|
||||
host: `sts.amazonaws.com`,
|
||||
path: `/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60`,
|
||||
headers: {
|
||||
'x-k8s-aws-id': clusterName,
|
||||
},
|
||||
signQuery: true,
|
||||
};
|
||||
const signedRequest = sign(request, {
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
sessionToken: credentials.sessionToken,
|
||||
});
|
||||
|
||||
return pipe([
|
||||
(signed: any) => `https://${signed.host}${signed.path}`,
|
||||
base64,
|
||||
removePadding,
|
||||
makeUrlSafe,
|
||||
prepend('k8s-aws-v1.'),
|
||||
])(signedRequest);
|
||||
}
|
||||
|
||||
async decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<ClusterDetails> {
|
||||
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
|
||||
{},
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
clusterDetailsWithAuthToken.serviceAccountToken = await this.getBearerToken(
|
||||
clusterDetails.name,
|
||||
);
|
||||
return clusterDetailsWithAuthToken;
|
||||
}
|
||||
}
|
||||
+8
@@ -18,6 +18,7 @@ import { KubernetesAuthTranslator } from './types';
|
||||
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
|
||||
import { KubernetesAuthTranslatorGenerator } from './KubernetesAuthTranslatorGenerator';
|
||||
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
|
||||
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
|
||||
|
||||
describe('getKubernetesAuthTranslatorInstance', () => {
|
||||
const sut = KubernetesAuthTranslatorGenerator;
|
||||
@@ -29,6 +30,13 @@ describe('getKubernetesAuthTranslatorInstance', () => {
|
||||
expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true);
|
||||
});
|
||||
|
||||
it('can return an auth translator for aws auth', () => {
|
||||
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
|
||||
'aws',
|
||||
);
|
||||
expect(authTranslator instanceof AwsIamKubernetesAuthTranslator).toBe(true);
|
||||
});
|
||||
|
||||
it('can return an auth translator for serviceAccount auth', () => {
|
||||
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
|
||||
'serviceAccount',
|
||||
|
||||
+4
@@ -17,6 +17,7 @@
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
|
||||
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
|
||||
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
|
||||
|
||||
export class KubernetesAuthTranslatorGenerator {
|
||||
static getKubernetesAuthTranslatorInstance(
|
||||
@@ -26,6 +27,9 @@ export class KubernetesAuthTranslatorGenerator {
|
||||
case 'google': {
|
||||
return new GoogleKubernetesAuthTranslator();
|
||||
}
|
||||
case 'aws': {
|
||||
return new AwsIamKubernetesAuthTranslator();
|
||||
}
|
||||
case 'serviceAccount': {
|
||||
return new ServiceAccountKubernetesAuthTranslator();
|
||||
}
|
||||
|
||||
@@ -148,4 +148,4 @@ export interface KubernetesFetchError {
|
||||
|
||||
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
|
||||
export type ClusterLocatorMethod = 'config';
|
||||
export type AuthProviderType = 'google' | 'serviceAccount';
|
||||
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
|
||||
|
||||
Reference in New Issue
Block a user