refactor k8s cluster picker
This commit is contained in:
parent
bf8323c383
commit
91d2ccbb95
9 changed files with 133 additions and 139 deletions
|
@ -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 />} />
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
)
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
export {GetK8sOIDCTokenExtension} from './extensions'
|
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
}),
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export { KubernetesClusterPickerExtension } from './extensions';
|
|
@ -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;
|
Loading…
Reference in a new issue