add cluster picker and credential retriever
This commit is contained in:
parent
383885df21
commit
d001ee0c17
12 changed files with 1962 additions and 1697 deletions
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"version": "1.12.1"
|
||||
"version": "1.14.2"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
14
packages/app/src/scaffolder/credentials/extensions.ts
Normal file
14
packages/app/src/scaffolder/credentials/extensions.ts
Normal 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
|
||||
}
|
||||
)
|
||||
)
|
63
packages/app/src/scaffolder/credentials/getOIDCToken.tsx
Normal file
63
packages/app/src/scaffolder/credentials/getOIDCToken.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
1
packages/app/src/scaffolder/credentials/index.ts
Normal file
1
packages/app/src/scaffolder/credentials/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {GetK8sOIDCTokenExtension} from './extensions'
|
20
packages/app/src/scaffolder/credentials/schema.ts
Normal file
20
packages/app/src/scaffolder/credentials/schema.ts
Normal 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
|
|
@ -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",
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue