modify approach to post entire entity to k8s backend

This commit is contained in:
Moustafa Baiou
2020-11-09 01:42:08 -05:00
committed by moustafab
parent 99fffed216
commit 4639d20720
15 changed files with 74 additions and 90 deletions
@@ -15,13 +15,13 @@
*/
import { KubernetesAuthTranslator } from './types';
import { AuthRequestBody, ClusterDetails } from '../types/types';
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
export class GoogleKubernetesAuthTranslator
implements KubernetesAuthTranslator {
async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
requestBody: AuthRequestBody,
requestBody: KubernetesRequestBody,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
@@ -15,7 +15,7 @@
*/
import { KubernetesAuthTranslator } from './types';
import { AuthRequestBody, ClusterDetails } from '../types/types';
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
export class ServiceAccountKubernetesAuthTranslator
implements KubernetesAuthTranslator {
@@ -23,7 +23,7 @@ export class ServiceAccountKubernetesAuthTranslator
clusterDetails: ClusterDetails,
// To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read.
// @ts-ignore-start
requestBody: AuthRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
requestBody: KubernetesRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
// @ts-ignore-end
): Promise<ClusterDetails> {
return clusterDetails;
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { AuthRequestBody, ClusterDetails } from '../types/types';
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
export interface KubernetesAuthTranslator {
decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
requestBody: AuthRequestBody,
requestBody: KubernetesRequestBody,
): Promise<ClusterDetails>;
}
@@ -91,7 +91,6 @@ describe('handleGetKubernetesObjectsForService', () => {
},
getVoidLogger(),
{},
'',
);
expect(getClustersByServiceId.mock.calls.length).toBe(1);
@@ -16,25 +16,25 @@
import { Logger } from 'winston';
import {
AuthRequestBody,
KubernetesRequestBody,
ClusterDetails,
KubernetesServiceLocator,
KubernetesFetcher,
KubernetesObjectTypes,
ObjectsByServiceIdResponse,
ObjectsByEntityResponse,
} from '../types/types';
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
import { ComponentEntityV1alpha1 } from '@backstage/catalog-model';
export type GetKubernetesObjectsForServiceHandler = (
serviceId: string,
fetcher: KubernetesFetcher,
serviceLocator: KubernetesServiceLocator,
logger: Logger,
requestBody: AuthRequestBody,
labelSelector: string,
requestBody: KubernetesRequestBody,
objectsToFetch?: Set<KubernetesObjectTypes>,
) => Promise<ObjectsByServiceIdResponse>;
) => Promise<ObjectsByEntityResponse>;
const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
'pods',
@@ -46,6 +46,26 @@ const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
'ingresses',
]);
function parseLabelSelector(entity: ComponentEntityV1alpha1): string {
if (
entity &&
entity.spec &&
entity.spec.kubernetes &&
entity.spec.kubernetes.selector
) {
// TODO: figure out how to convert the selector to the full query param from the yaml
// (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go)
const { matchLabels } = entity.spec.kubernetes.selector;
if (!matchLabels) {
return '';
}
return Object.keys(matchLabels)
.map(key => `${key}=${matchLabels[key.toString()]}`)
.join(',');
}
return '';
}
// Fans out the request to all clusters that the service lives in, aggregates their responses together
export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServiceHandler = async (
serviceId,
@@ -53,7 +73,6 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic
serviceLocator,
logger,
requestBody,
labelSelector: string,
objectsToFetch = DEFAULT_OBJECTS,
) => {
const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId(
@@ -80,6 +99,8 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic
.join(', ')}]`,
);
const labelSelector = parseLabelSelector(requestBody.entity);
return Promise.all(
clusterDetailsDecoratedForAuth.map(cd => {
return fetcher
@@ -21,14 +21,14 @@ import { makeRouter } from './router';
import {
KubernetesServiceLocator,
KubernetesFetcher,
ObjectsByServiceIdResponse,
ObjectsByEntityResponse,
} from '..';
describe('router', () => {
let app: express.Express;
let kubernetesFetcher: jest.Mocked<KubernetesFetcher>;
let kubernetesServiceLocator: jest.Mocked<KubernetesServiceLocator>;
let handleGetByServiceId: jest.Mock<Promise<ObjectsByServiceIdResponse>>;
let handleGetByServiceId: jest.Mock<Promise<ObjectsByEntityResponse>>;
beforeAll(async () => {
kubernetesFetcher = {
@@ -26,7 +26,7 @@ import {
handleGetKubernetesObjectsForService,
} from './getKubernetesObjectsForServiceHandler';
import {
AuthRequestBody,
KubernetesRequestBody,
KubernetesServiceLocator,
KubernetesFetcher,
ServiceLocatorMethod,
@@ -64,23 +64,21 @@ export const makeRouter = (
logger: Logger,
fetcher: KubernetesFetcher,
serviceLocator: KubernetesServiceLocator,
handleGetByServiceId: GetKubernetesObjectsForServiceHandler,
handleGetByEntity: GetKubernetesObjectsForServiceHandler,
): express.Router => {
const router = Router();
router.use(express.json());
router.post('/services/:serviceId', async (req, res) => {
const serviceId = req.params.serviceId;
const labelSelector = req.query.labelSelector;
const requestBody: AuthRequestBody = req.body;
const requestBody: KubernetesRequestBody = req.body;
try {
const response = await handleGetByServiceId(
const response = await handleGetByEntity(
serviceId,
fetcher,
serviceLocator,
logger,
requestBody,
labelSelector ? labelSelector.toString() : '',
);
res.send(response);
} catch (e) {
@@ -23,6 +23,7 @@ import {
V1ReplicaSet,
V1Service,
} from '@kubernetes/client-node';
import { ComponentEntityV1alpha1 } from '@backstage/catalog-model';
export interface ClusterDetails {
name: string;
@@ -31,10 +32,11 @@ export interface ClusterDetails {
serviceAccountToken?: string | undefined;
}
export interface AuthRequestBody {
export interface KubernetesRequestBody {
auth?: {
google?: string;
};
entity: ComponentEntityV1alpha1;
}
export interface ClusterObjects {
@@ -43,7 +45,7 @@ export interface ClusterObjects {
errors: KubernetesFetchError[];
}
export interface ObjectsByServiceIdResponse {
export interface ObjectsByEntityResponse {
items: ClusterObjects[];
}
@@ -17,10 +17,9 @@
import { DiscoveryApi } from '@backstage/core';
import { KubernetesApi } from './types';
import {
AuthRequestBody,
ObjectsByServiceIdResponse,
KubernetesRequestBody,
ObjectsByEntityResponse,
} from '@backstage/plugin-kubernetes-backend';
import { V1LabelSelector } from '@kubernetes/client-node';
export class KubernetesBackendClient implements KubernetesApi {
private readonly discoveryApi: DiscoveryApi;
@@ -31,7 +30,7 @@ export class KubernetesBackendClient implements KubernetesApi {
private async getRequired(
path: string,
requestBody: AuthRequestBody,
requestBody: KubernetesRequestBody,
): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
const response = await fetch(url, {
@@ -51,26 +50,11 @@ export class KubernetesBackendClient implements KubernetesApi {
return await response.json();
}
private parseLabelSelector(params: V1LabelSelector): string {
// TODO: figure out how to convert the selector to the full query param from the yaml
// (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go)
const { matchLabels } = params;
if (!matchLabels) {
return '';
}
return Object.keys(matchLabels)
.map(key => `${key}=${matchLabels[key.toString()]}`)
.join(',');
}
async getObjectsByLabelSelector(
serviceId: String,
labelSelector: V1LabelSelector,
requestBody: AuthRequestBody,
): Promise<ObjectsByServiceIdResponse> {
const labelSelectorQueryParams = this.parseLabelSelector(labelSelector);
requestBody: KubernetesRequestBody,
): Promise<ObjectsByEntityResponse> {
return await this.getRequired(
`/services/${serviceId}?labelSelector=${labelSelectorQueryParams}`,
`/services/${requestBody.entity.metadata.name}`,
requestBody,
);
}
+4 -6
View File
@@ -16,8 +16,8 @@
import { createApiRef } from '@backstage/core';
import {
AuthRequestBody,
ObjectsByServiceIdResponse,
KubernetesRequestBody,
ObjectsByEntityResponse,
} from '@backstage/plugin-kubernetes-backend';
import { V1LabelSelector } from '@kubernetes/client-node';
@@ -29,8 +29,6 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
export interface KubernetesApi {
getObjectsByLabelSelector(
serviceId: String,
labelSelector: V1LabelSelector,
requestBody: AuthRequestBody,
): Promise<ObjectsByServiceIdResponse>;
requestBody: KubernetesRequestBody,
): Promise<ObjectsByEntityResponse>;
}
@@ -29,10 +29,10 @@ import {
import { ComponentEntityV1alpha1, Entity } from '@backstage/catalog-model';
import { kubernetesApiRef } from '../../api/types';
import {
AuthRequestBody,
KubernetesRequestBody,
ClusterObjects,
FetchResponse,
ObjectsByServiceIdResponse,
ObjectsByEntityResponse,
} from '@backstage/plugin-kubernetes-backend';
import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types';
import { DeploymentTables } from '../DeploymentTables';
@@ -105,7 +105,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
const kubernetesApi = useApi(kubernetesApiRef);
const [kubernetesObjects, setKubernetesObjects] = useState<
ObjectsByServiceIdResponse | undefined
ObjectsByEntityResponse | undefined
>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
@@ -121,7 +121,9 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
useEffect(() => {
(async () => {
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
let requestBody: AuthRequestBody = {};
let requestBody: KubernetesRequestBody = {
entity: entity as ComponentEntityV1alpha1,
};
for (const authProviderStr of authProviders) {
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
@@ -130,29 +132,9 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
);
}
// decide label selector to search by defaulting to this label
let labelSelector: V1LabelSelector = {
matchLabels: {
'backstage.io/kubernetes-id': entity.metadata.name,
},
};
const componentEntity = entity as ComponentEntityV1alpha1;
if (
componentEntity.spec &&
componentEntity.spec.kubernetes &&
(componentEntity.spec.kubernetes.selector as V1LabelSelector)
) {
labelSelector = componentEntity.spec.kubernetes.selector;
}
// TODO: Add validation on contents/format of requestBody
kubernetesApi
.getObjectsByLabelSelector(
entity.metadata.name,
labelSelector,
requestBody,
)
.getObjectsByLabelSelector(requestBody)
.then(result => {
setKubernetesObjects(result);
})
@@ -16,7 +16,7 @@
import { OAuthApi } from '@backstage/core';
import { KubernetesAuthProvider } from './types';
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
authProvider: OAuthApi;
@@ -26,8 +26,8 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
}
async decorateRequestBodyForAuth(
requestBody: AuthRequestBody,
): Promise<AuthRequestBody> {
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
const googleAuthToken: string = await this.authProvider.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
);
@@ -15,7 +15,7 @@
*/
import { OAuthApi } from '@backstage/core';
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types';
import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
@@ -40,8 +40,8 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
async decorateRequestBodyForAuth(
authProvider: string,
requestBody: AuthRequestBody,
): Promise<AuthRequestBody> {
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
const kubernetesAuthProvider:
| KubernetesAuthProvider
| undefined = this.kubernetesAuthProviderMap.get(authProvider);
@@ -15,13 +15,13 @@
*/
import { KubernetesAuthProvider } from './types';
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
export class ServiceAccountKubernetesAuthProvider
implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: AuthRequestBody,
): Promise<AuthRequestBody> {
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
// No-op, with service account for auth, cluster config/details should already have serviceAccountToken
return requestBody;
}
@@ -15,12 +15,12 @@
*/
import { createApiRef } from '@backstage/core';
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
export interface KubernetesAuthProvider {
decorateRequestBodyForAuth(
requestBody: AuthRequestBody,
): Promise<AuthRequestBody>;
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
}
export const kubernetesAuthProvidersApiRef = createApiRef<
@@ -33,6 +33,6 @@ export const kubernetesAuthProvidersApiRef = createApiRef<
export interface KubernetesAuthProvidersApi {
decorateRequestBodyForAuth(
authProvider: string,
requestBody: AuthRequestBody,
): Promise<AuthRequestBody>;
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
}