add cluster picker and credential retriever

This commit is contained in:
Manabu Mccloskey 2023-06-16 15:49:51 -07:00
parent 383885df21
commit d001ee0c17
12 changed files with 1962 additions and 1697 deletions

View file

@ -1,3 +1,3 @@
{
"version": "1.12.1"
"version": "1.14.2"
}

View file

@ -29,7 +29,7 @@
]
},
"devDependencies": {
"@backstage/cli": "^0.22.5",
"@backstage/cli": "^0.22.7",
"@spotify/prettier-config": "^12.0.0",
"concurrently": "^6.0.0",
"lerna": "^4.0.0",

View file

@ -18,35 +18,37 @@
"cy:run": "cypress run --browser chrome"
},
"dependencies": {
"@backstage/app-defaults": "^1.2.1",
"@backstage/catalog-model": "^1.2.1",
"@backstage/cli": "^0.22.5",
"@backstage/core-app-api": "^1.6.0",
"@backstage/core-components": "^0.12.5",
"@backstage/core-plugin-api": "^1.5.0",
"@backstage/integration-react": "^1.1.11",
"@backstage/plugin-api-docs": "^0.9.1",
"@backstage/plugin-catalog": "^1.9.0",
"@backstage/plugin-catalog-common": "^1.0.12",
"@backstage/plugin-catalog-graph": "^0.2.28",
"@backstage/plugin-catalog-import": "^0.9.6",
"@backstage/plugin-catalog-react": "^1.4.0",
"@backstage/plugin-github-actions": "^0.5.16",
"@backstage/plugin-kubernetes": "^0.7.9",
"@backstage/plugin-org": "^0.6.6",
"@backstage/plugin-permission-react": "^0.4.11",
"@backstage/plugin-scaffolder": "^1.12.0",
"@backstage/plugin-search": "^1.1.1",
"@backstage/plugin-search-react": "^1.5.1",
"@backstage/plugin-tech-radar": "^0.6.2",
"@backstage/plugin-techdocs": "^1.6.0",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.0.11",
"@backstage/plugin-techdocs-react": "^1.1.4",
"@backstage/plugin-user-settings": "^0.7.1",
"@backstage/theme": "^0.2.18",
"@backstage/app-defaults": "^1.3.1",
"@backstage/catalog-model": "^1.3.0",
"@backstage/cli": "^0.22.7",
"@backstage/core-app-api": "^1.8.0",
"@backstage/core-components": "^0.13.1",
"@backstage/core-plugin-api": "^1.5.1",
"@backstage/integration-react": "^1.1.13",
"@backstage/plugin-api-docs": "^0.9.4",
"@backstage/plugin-catalog": "^1.11.1",
"@backstage/plugin-catalog-common": "^1.0.13",
"@backstage/plugin-catalog-graph": "^0.2.30",
"@backstage/plugin-catalog-import": "^0.9.8",
"@backstage/plugin-catalog-react": "^1.6.0",
"@backstage/plugin-github-actions": "^0.5.18",
"@backstage/plugin-kubernetes": "^0.9.1",
"@backstage/plugin-org": "^0.6.8",
"@backstage/plugin-permission-react": "^0.4.12",
"@backstage/plugin-scaffolder": "^1.13.1",
"@backstage/plugin-scaffolder-react": "^1.4.0",
"@backstage/plugin-search": "^1.3.1",
"@backstage/plugin-search-react": "^1.6.1",
"@backstage/plugin-tech-radar": "^0.6.4",
"@backstage/plugin-techdocs": "^1.6.3",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.0.13",
"@backstage/plugin-techdocs-react": "^1.1.6",
"@backstage/plugin-user-settings": "^0.7.3",
"@backstage/theme": "^0.3.0",
"@internal/plugin-workflows": "^0.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@rjsf/core": "^5.8.1",
"history": "^5.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@ -54,7 +56,7 @@
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/test-utils": "^1.2.6",
"@backstage/test-utils": "^1.3.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",

View file

@ -11,6 +11,7 @@ import {
catalogImportPlugin,
} from '@backstage/plugin-catalog-import';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import {ScaffolderFieldExtensions} from '@backstage/plugin-scaffolder-react'
import { orgPlugin } from '@backstage/plugin-org';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
@ -34,6 +35,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";
const app = createApp({
apis,
@ -88,7 +90,11 @@ const routes = (
<ReportIssue />
</TechDocsAddons>
</Route>
<Route path="/create" element={<ScaffolderPage />} />
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<GetK8sOIDCTokenExtension />
</ScaffolderFieldExtensions>
</Route>
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route
path="/tech-radar"

View file

@ -0,0 +1,14 @@
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

@ -0,0 +1,63 @@
import React 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 GetK8sOIDCToken = (props: ClusterPickerProps) => {
const k8sApi = useApi(kubernetesApiRef)
const k8sAuthProviderApi = useApi(kubernetesAuthProvidersApiRef)
const { setSecrets } = useTemplateSecrets();
const {uiSchema, onChange, rawErrors, formData, required} = props
const getToken = async () => {
const {requestUserCredentials} = uiSchema?.['ui:options'] ?? {}
if (!requestUserCredentials) {
return;
}
const clusters = await k8sApi.getClusters()
const cluster = clusters.find(c => {
return c.name === formData
})
const creds = await k8sAuthProviderApi.getCredentials(cluster!.oidcTokenProvider!)
setSecrets({ [requestUserCredentials.secretKey]: creds.token! })
}
const {value: {clusters} = {clusters: []}, loading } = useAsync(
async () => {
const c = await k8sApi.getClusters()
return {clusters: c.map(i => ({ label: i.name, value: i.name}))}
}
)
if (loading) {
return <Progress />;
}
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<Select
native
label="Cluster"
items={clusters}
onChange={getToken}
/>
<FormHelperText id="entityName">
Kubernetes Cluster Name
</FormHelperText>
</FormControl>
);
};

View file

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

View file

@ -0,0 +1,20 @@
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

@ -16,28 +16,28 @@
"build-image": "docker build ../.. -f Dockerfile --tag backstage"
},
"dependencies": {
"@backstage/backend-common": "^0.18.3",
"@backstage/backend-tasks": "^0.5.0",
"@backstage/catalog-client": "^1.4.0",
"@backstage/catalog-model": "^1.2.1",
"@backstage/backend-common": "^0.18.5",
"@backstage/backend-tasks": "^0.5.2",
"@backstage/catalog-client": "^1.4.1",
"@backstage/catalog-model": "^1.3.0",
"@backstage/config": "^1.0.7",
"@backstage/integration": "^1.4.3",
"@backstage/plugin-app-backend": "^0.3.43",
"@backstage/plugin-auth-backend": "^0.18.1",
"@backstage/plugin-auth-node": "^0.2.12",
"@backstage/plugin-catalog-backend": "^1.8.0",
"@backstage/plugin-kubernetes-backend": "^0.9.4",
"@backstage/plugin-permission-common": "^0.7.4",
"@backstage/plugin-permission-node": "^0.7.6",
"@backstage/plugin-proxy-backend": "^0.2.37",
"@backstage/plugin-scaffolder-backend": "^1.12.0",
"@backstage/plugin-scaffolder-node": "^0.1.1",
"@backstage/plugin-search-backend": "^1.2.4",
"@backstage/plugin-search-backend-module-pg": "^0.5.4",
"@backstage/plugin-search-backend-node": "^1.1.4",
"@backstage/plugin-techdocs-backend": "^1.6.0",
"@backstage/integration": "^1.4.5",
"@backstage/plugin-app-backend": "^0.3.45",
"@backstage/plugin-auth-backend": "^0.18.3",
"@backstage/plugin-auth-node": "^0.2.14",
"@backstage/plugin-catalog-backend": "^1.9.1",
"@backstage/plugin-kubernetes-backend": "^0.11.0",
"@backstage/plugin-permission-common": "^0.7.5",
"@backstage/plugin-permission-node": "^0.7.8",
"@backstage/plugin-proxy-backend": "^0.2.39",
"@backstage/plugin-scaffolder-backend": "^1.14.0",
"@backstage/plugin-scaffolder-node": "^0.1.3",
"@backstage/plugin-search-backend": "^1.3.1",
"@backstage/plugin-search-backend-module-pg": "^0.5.6",
"@backstage/plugin-search-backend-node": "^1.2.1",
"@backstage/plugin-techdocs-backend": "^1.6.2",
"@kubernetes/client-node": "^0.18.1",
"@roadiehq/scaffolder-backend-module-utils": "^1.8.4",
"@roadiehq/scaffolder-backend-module-utils": "^1.8.7",
"app": "link:../app",
"better-sqlite3": "^8.0.0",
"dockerode": "^3.3.1",
@ -47,7 +47,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.22.5",
"@backstage/cli": "^0.22.7",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",

View file

@ -3,6 +3,8 @@ import { Config } from '@backstage/config';
import * as k8s from '@kubernetes/client-node';
import {Logger} from "winston";
import {HttpError} from "@kubernetes/client-node";
import {useApi} from "@backstage/core-plugin-api";
import {OidcKubernetesAuthTranslator} from "@backstage/plugin-kubernetes-backend";
type argoInput = {
namespace: string
@ -142,66 +144,69 @@ export function createInvokeArgoAction(config: Config, logger: Logger) {
},
async handler(ctx: ActionContext<argoInput>) {
logger.debug(`Invoked with ${JSON.stringify(ctx.input)})`)
const targetCluster = getClusterConfig(ctx.input.clusterName, config)
const kc = new k8s.KubeConfig()
kc.addCluster({
name: ctx.input.clusterName,
caData: targetCluster.getString("caData"),
server: targetCluster.getString("url"),
skipTLSVerify: targetCluster.getBoolean("skipTLSVerify"),
})
kc.addUser({
name: "admin",
token: targetCluster.getString("serviceAccountToken")
})
kc.addContext({
cluster: ctx.input.clusterName,
user: "admin",
name: ctx.input.clusterName
})
kc.setCurrentContext(ctx.input.clusterName)
const client = kc.makeApiClient(k8s.CustomObjectsApi)
const wf = new Workflow(ctx.input.templateName, ctx.input.namespace, ctx.input.parameters)
// const body = generateBody(ctx.input.templateName, ctx.input.namespace)
try {
const resp = await client.createNamespacedCustomObject(
argoWorkflowsGroup, argoWorkflowsVersion, ctx.input.namespace,
argoWorkFlowPlural, wf
)
const respBody = resp.body as Workflow
logger.debug(`Workflow ID: ${respBody.metadata.name}, namespace ${respBody.metadata.namespace}`)
ctx.output('workflowName', respBody.metadata.name!)
ctx.output('workflowNamespace', respBody.metadata.namespace!)
if (ctx.input.wait) {
await wait(kc, respBody.metadata.namespace!, respBody.metadata.name!)
}
} catch (err) {
if (err instanceof HttpError) {
let msg = `${err.response.statusMessage}: `
if ("kind" in err.body && err.body.kind === "Status" && "message" in err.body) {
msg += err.body.message
}
logger.info(`error : ${err.response.statusCode} ${msg}`)
throw new Error(`Failed to talk to the cluster: ${err.response.statusCode} ${err.response.statusMessage} \n ${msg}`)
}
if (err instanceof Error) {
logger.error(`error while talking to cluster: ${err.name} ${err.message}`)
}
throw new Error("Unknown exception was encountered.")
}
logger.info(JSON.stringify(ctx.secrets))
// const targetCluster = getClusterConfig(ctx.input.clusterName, config)
// const kc = new k8s.KubeConfig()
// kc.addCluster({
// name: ctx.input.clusterName,
// caData: targetCluster.getString("caData"),
// server: targetCluster.getString("url"),
// skipTLSVerify: targetCluster.getBoolean("skipTLSVerify"),
// })
// kc.addUser({
// name: "admin",
// token: targetCluster.getString("serviceAccountToken")
// })
// kc.addContext({
// cluster: ctx.input.clusterName,
// user: "admin",
// name: ctx.input.clusterName
// })
// kc.setCurrentContext(ctx.input.clusterName)
//
// const client = kc.makeApiClient(k8s.CustomObjectsApi)
// const wf = new Workflow(ctx.input.templateName, ctx.input.namespace, ctx.input.parameters)
// // const body = generateBody(ctx.input.templateName, ctx.input.namespace)
// try {
// const resp = await client.createNamespacedCustomObject(
// argoWorkflowsGroup, argoWorkflowsVersion, ctx.input.namespace,
// argoWorkFlowPlural, wf
// )
// const respBody = resp.body as Workflow
// logger.debug(`Workflow ID: ${respBody.metadata.name}, namespace ${respBody.metadata.namespace}`)
// ctx.output('workflowName', respBody.metadata.name!)
// ctx.output('workflowNamespace', respBody.metadata.namespace!)
// if (ctx.input.wait) {
// await wait(kc, respBody.metadata.namespace!, respBody.metadata.name!)
// }
// } catch (err) {
// if (err instanceof HttpError) {
// let msg = `${err.response.statusMessage}: `
// if ("kind" in err.body && err.body.kind === "Status" && "message" in err.body) {
// msg += err.body.message
// }
// logger.info(`error : ${err.response.statusCode} ${msg}`)
// throw new Error(`Failed to talk to the cluster: ${err.response.statusCode} ${err.response.statusMessage} \n ${msg}`)
// }
// if (err instanceof Error) {
// logger.error(`error while talking to cluster: ${err.name} ${err.message}`)
// }
// throw new Error("Unknown exception was encountered.")
// }
}
}
)
}
function getClusterConfig(name: string, config: Config): Config {
const at = new OidcKubernetesAuthTranslator();
const c = config.getConfigArray("kubernetes.clusterLocatorMethods")
const cc = c.filter(function(val) {
return val.getString("type") === "config"
})
const clusters = new Array<Config>();
// this is shit
cc.forEach(function(conf ) {

View file

@ -23,10 +23,10 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "^0.12.5",
"@backstage/core-plugin-api": "^1.5.0",
"@backstage/plugin-catalog-react": "^1.4.0",
"@backstage/theme": "^0.2.18",
"@backstage/core-components": "^0.13.1",
"@backstage/core-plugin-api": "^1.5.1",
"@backstage/plugin-catalog-react": "^1.6.0",
"@backstage/theme": "^0.3.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@ -36,10 +36,10 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.22.5",
"@backstage/core-app-api": "^1.6.0",
"@backstage/dev-utils": "^1.0.13",
"@backstage/test-utils": "^1.2.6",
"@backstage/cli": "^0.22.7",
"@backstage/core-app-api": "^1.8.0",
"@backstage/dev-utils": "^1.0.15",
"@backstage/test-utils": "^1.3.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",

3332
yarn.lock

File diff suppressed because it is too large Load diff