refactor k8s cluster picker

This commit is contained in:
Manabu Mccloskey 2023-07-25 18:00:42 -07:00
parent bf8323c383
commit 91d2ccbb95
9 changed files with 133 additions and 139 deletions

View file

@ -38,7 +38,7 @@ import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
import { GetK8sOIDCTokenExtension } from './scaffolder/credentials';
import { KubernetesClusterPickerExtension } from './scaffolder/kubernetesClusterPicker';
const app = createApp({
apis,
@ -95,7 +95,7 @@ const routes = (
</Route>
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<GetK8sOIDCTokenExtension />
<KubernetesClusterPickerExtension />
</ScaffolderFieldExtensions>
</Route>
<Route path="/api-docs" element={<ApiExplorerPage />} />

View file

@ -1,14 +0,0 @@
import {scaffolderPlugin,} from '@backstage/plugin-scaffolder';
import {createScaffolderFieldExtension} from "@backstage/plugin-scaffolder-react";
import {GetK8sOIDCToken} from "./getOIDCToken";
import {ClusterPickerSchema} from "./schema";
export const GetK8sOIDCTokenExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension(
{
name: 'GetK8sOIDCToken',
component: GetK8sOIDCToken,
schema: ClusterPickerSchema,
}
)
)

View file

@ -1,102 +0,0 @@
import React, {useCallback, useEffect} from 'react';
import FormControl from '@material-ui/core/FormControl';
import {
useApi, configApiRef, discoveryApiRef, oauthRequestApiRef
} from "@backstage/core-plugin-api";
import { kubernetesApiRef } from "@backstage/plugin-kubernetes";
import { FormHelperText } from "@material-ui/core";
import {Progress, Select} from "@backstage/core-components";
import useAsync from "react-use/lib/useAsync";
import {useTemplateSecrets} from "@backstage/plugin-scaffolder-react";
import {ClusterPickerProps} from "./schema";
import {OAuth2} from "@backstage/core-app-api";
export const GetK8sOIDCToken = (props: ClusterPickerProps) => {
const k8sApi = useApi(kubernetesApiRef)
const { setSecrets, secrets } = useTemplateSecrets();
const discoveryApi = useApi(discoveryApiRef)
const oauthRequestApi = useApi(oauthRequestApiRef)
const configApi = useApi(configApiRef)
const {uiSchema, required} = props
let {rawErrors} = props
const {value: {clusters} = {clusters: []}, loading } = useAsync(
async () => {
const c = await k8sApi.getClusters()
return {clusters: c.map(i => ({ label: i.name, value: i.name}))}
}
)
if (!rawErrors) {
rawErrors = []
}
const getToken = useCallback( async (clusterName: string) => {
const {requestUserCredentials} = uiSchema?.['ui:options'] ?? {}
if (!requestUserCredentials) {
return;
}
const cs = await k8sApi.getClusters()
const cluster = cs.find(c => {
return c.name === clusterName
})
if (cluster?.oidcTokenProvider === undefined) {
throw new Error("no oidc provider defined for this cluster")
}
const oidc = OAuth2.create({
discoveryApi,
oauthRequestApi,
provider: {
id: cluster.oidcTokenProvider,
title: 'OIDC',
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
defaultScopes: ['openid', 'profile', 'email', 'groups'],
})
const token = await oidc.getIdToken()
setSecrets({ [requestUserCredentials.secretKey]: token })
}, [configApi, discoveryApi, k8sApi, oauthRequestApi, setSecrets, uiSchema]
)
useEffect(() => {
const {requestUserCredentials} = uiSchema?.['ui:options'] ?? {}
if (!requestUserCredentials?.secretKey || secrets[requestUserCredentials?.secretKey!]) {
return
}
if (clusters.length) {
getToken(clusters[0].value).catch(console.error)
}
}, [clusters, getToken, secrets, uiSchema])
if (loading) {
return <Progress />;
}
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0}
>
<Select
native
label="Cluster"
items={clusters}
onChange={e => getToken(e.toString())}
placeholder="select one"
/>
<FormHelperText id="entityName">
Kubernetes Cluster Name
</FormHelperText>
</FormControl>
);
};

View file

@ -1 +0,0 @@
export {GetK8sOIDCTokenExtension} from './extensions'

View file

@ -1,20 +0,0 @@
import { z } from 'zod';
import {makeFieldSchemaFromZod} from "@backstage/plugin-scaffolder";
export const ClusterPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object( {
requestUserCredentials: z.object({
secretKey: z.string().describe('Key used within the template secrets context to store the credential')
}
)
.optional()
.describe('If defined will request user credentials to auth against the cluster')
})
)
export const ClusterPickerSchema = ClusterPickerFieldSchema.schema
export type ClusterPickerProps = typeof ClusterPickerFieldSchema.type
export type ClusterPickerUiOptions = typeof ClusterPickerFieldSchema.uiOptionsType

View file

@ -0,0 +1,87 @@
import React, { useCallback, useMemo } from 'react';
import FormControl from '@material-ui/core/FormControl';
import { useApi } from '@backstage/core-plugin-api';
import {
kubernetesApiRef,
kubernetesAuthProvidersApiRef,
} from '@backstage/plugin-kubernetes';
import { FormHelperText } from '@material-ui/core';
import { Progress, Select } from '@backstage/core-components';
import useAsync from 'react-use/lib/useAsync';
import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react';
import { ClusterPickerProps } from './schema';
export const KubernetesClusterPicker = (props: ClusterPickerProps) => {
const k8sApi = useApi(kubernetesApiRef);
const k8sAuthApi = useApi(kubernetesAuthProvidersApiRef);
const { setSecrets } = useTemplateSecrets();
const { uiSchema, required, onChange } = props;
const { rawErrors } = props ?? [];
const allowedClusters = useMemo(
() => uiSchema?.['ui:options']?.allowedClusters ?? [],
[uiSchema],
);
const getToken = useCallback(
async (clusterName: string) => {
const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {};
if (!requestUserCredentials) {
return;
}
const cs = await k8sApi.getClusters();
const cluster = cs.find(c => {
return c.name === clusterName;
});
const { token } = await k8sAuthApi.getCredentials(cluster?.authProvider!);
if (token === undefined) {
return;
}
setSecrets({ [requestUserCredentials.secretKey]: token });
},
[k8sApi, k8sAuthApi, setSecrets, uiSchema],
);
const { value: { clusters } = { clusters: [] }, loading } = useAsync(
async () => {
const c = await k8sApi.getClusters();
const filteredClusters = c
.filter(i => {
if (allowedClusters.length === 0) {
return true;
}
return allowedClusters.includes(i.name);
})
.map(i => ({ label: i.name, value: i.name }));
if (filteredClusters.length) {
await getToken(filteredClusters[0].value);
onChange(filteredClusters[0].value);
}
return {
clusters: filteredClusters,
};
},
);
if (loading) {
return <Progress />;
}
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0}
>
<Select
native
label="Cluster"
items={clusters}
onChange={e => {
onChange(e.toString());
getToken(e.toString());
}}
/>
<FormHelperText>Kubernetes Cluster Name</FormHelperText>
</FormControl>
);
};

View file

@ -0,0 +1,12 @@
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
import { KubernetesClusterPicker } from './KubernetesClusterPicker';
import { ClusterPickerSchema } from './schema';
export const KubernetesClusterPickerExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'KubernetesClusterPicker',
component: KubernetesClusterPicker,
schema: ClusterPickerSchema,
}),
);

View file

@ -0,0 +1 @@
export { KubernetesClusterPickerExtension } from './extensions';

View file

@ -0,0 +1,31 @@
import { z } from 'zod';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
export const ClusterPickerFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
requestUserCredentials: z
.object({
secretKey: z
.string()
.describe(
'Key used within the template secrets context to store the credential',
),
})
.optional()
.describe(
'If defined will request user credentials to auth against the cluster',
),
allowedClusters: z
.array(z.string())
.optional()
.describe('List of allowed Kubernetes clusters'),
}),
);
export const ClusterPickerSchema = ClusterPickerFieldSchema.schema;
export type ClusterPickerProps = typeof ClusterPickerFieldSchema.type;
export type ClusterPickerUiOptions =
typeof ClusterPickerFieldSchema.uiOptionsType;