diff --git a/app-config.yaml b/app-config.yaml index 3b5de8b..a61eb3e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -37,9 +37,12 @@ backend: integrations: github: - host: github.com - # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information - # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration - token: ${GITHUB_TOKEN} + apps: + - $include: github-integration.yaml +# - host: github.com +# # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information +# # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration +# token: ${GITHUB_TOKEN} ### Example for how to add your GitHub Enterprise instance using the API: # - host: ghe.example.net # apiBaseUrl: https://ghe.example.net/api/v3 @@ -75,12 +78,15 @@ catalog: entityFilename: catalog-info.yaml pullRequestBranchName: backstage-integration rules: - - allow: [Component, System, API, Resource, Location] + - allow: [Component, System, API, Resource, Location, Template] locations: # Local example data, file locations are relative to the backend process, typically `packages/backend` - type: file target: ../../examples/entities.yaml - + - type: file + target: /Users/mccloman/repos/backstage-templates/template1.yaml + - type: file + target: /Users/mccloman/repos/backstage-app/backstage/test-template.yaml # Local example template - type: file target: ../../examples/template/template.yaml @@ -102,3 +108,8 @@ catalog: # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml # rules: # - allow: [User, Group] +kubernetes: + serviceLocatorMethod: + type: 'multiTenant' + clusterLocatorMethods: + - $include: k8s-config.yaml diff --git a/packages/app/package.json b/packages/app/package.json index a810e10..2ee2120 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -32,6 +32,7 @@ "@backstage/plugin-catalog-import": "^0.9.5", "@backstage/plugin-catalog-react": "^1.3.0", "@backstage/plugin-github-actions": "^0.5.15", + "@backstage/plugin-kubernetes": "^0.7.8", "@backstage/plugin-org": "^0.6.5", "@backstage/plugin-permission-react": "^0.4.10", "@backstage/plugin-scaffolder": "^1.11.0", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 54a05ee..80c2aa8 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -56,6 +56,8 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { EntityKubernetesContent } from '@backstage/plugin-kubernetes'; + const techdocsContent = ( @@ -342,6 +344,9 @@ const systemPage = ( unidirectional={false} /> + + + ); diff --git a/packages/backend/package.json b/packages/backend/package.json index d3591f7..d22920e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -21,18 +21,22 @@ "@backstage/catalog-client": "^1.3.1", "@backstage/catalog-model": "^1.2.0", "@backstage/config": "^1.0.6", + "@backstage/integration": "^1.4.2", "@backstage/plugin-app-backend": "^0.3.42", "@backstage/plugin-auth-backend": "^0.18.0", "@backstage/plugin-auth-node": "^0.2.11", "@backstage/plugin-catalog-backend": "^1.7.2", + "@backstage/plugin-kubernetes-backend": "^0.9.3", "@backstage/plugin-permission-common": "^0.7.3", "@backstage/plugin-permission-node": "^0.7.5", "@backstage/plugin-proxy-backend": "^0.2.36", "@backstage/plugin-scaffolder-backend": "^1.11.0", + "@backstage/plugin-scaffolder-node": "^0.1.0", "@backstage/plugin-search-backend": "^1.2.3", "@backstage/plugin-search-backend-module-pg": "^0.5.3", "@backstage/plugin-search-backend-node": "^1.1.3", "@backstage/plugin-techdocs-backend": "^1.5.3", + "@kubernetes/client-node": "^0.18.1", "app": "link:../app", "better-sqlite3": "^8.0.0", "dockerode": "^3.3.1", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index c4736a5..7c25d57 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -32,6 +32,8 @@ import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; +import kubernetes from './plugins/kubernetes'; + function makeCreateEnv(config: Config) { const root = getRootLogger(); const reader = UrlReaders.default({ logger: root, config }); @@ -86,6 +88,8 @@ async function main() { const searchEnv = useHotMemoize(module, () => createEnv('search')); const appEnv = useHotMemoize(module, () => createEnv('app')); + const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); + const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); @@ -94,6 +98,8 @@ async function main() { apiRouter.use('/proxy', await proxy(proxyEnv)); apiRouter.use('/search', await search(searchEnv)); + apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); + // Add backends ABOVE this line; this 404 handler is the catch-all fallback apiRouter.use(notFoundHandler()); diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts new file mode 100644 index 0000000..60991d3 --- /dev/null +++ b/packages/backend/src/plugins/kubernetes.ts @@ -0,0 +1,16 @@ +import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const catalogApi = new CatalogClient({discoveryApi: env.discovery}); + const {router} = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, + catalogApi, + }).build(); + return router; +} diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index ef46f07..f052114 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -1,7 +1,9 @@ import { CatalogClient } from '@backstage/catalog-client'; -import { createRouter } from '@backstage/plugin-scaffolder-backend'; +import {createBuiltinActions, createRouter} from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; +import { ScmIntegrations } from '@backstage/integration'; +import {createInvokeArgoAction} from './workflow-argo' export default async function createPlugin( env: PluginEnvironment, @@ -9,13 +11,25 @@ export default async function createPlugin( const catalogClient = new CatalogClient({ discoveryApi: env.discovery, }); + const integrations = ScmIntegrations.fromConfig(env.config); + + const builtInActions = createBuiltinActions({ + integrations, + catalogClient, + config: env.config, + reader: env.reader, + }); + console.log(`env.logger ${env.logger}`) + env.logger.info("HIIIII") + const actions = [...builtInActions, createInvokeArgoAction(env.config, env.logger)]; return await createRouter({ + actions: actions, logger: env.logger, config: env.config, database: env.database, reader: env.reader, - catalogClient, + catalogClient: catalogClient, identity: env.identity, }); } diff --git a/packages/backend/src/plugins/workflow-argo.ts b/packages/backend/src/plugins/workflow-argo.ts new file mode 100644 index 0000000..eaec485 --- /dev/null +++ b/packages/backend/src/plugins/workflow-argo.ts @@ -0,0 +1,111 @@ +import {ActionContext, createTemplateAction} from "@backstage/plugin-scaffolder-node"; +import { Config } from '@backstage/config'; +import * as k8s from '@kubernetes/client-node'; +import {Logger} from "winston"; +import {HttpError} from "@kubernetes/client-node"; + +// export function createInvokeArgoAction() { + + +type argoInput = { + name: string + namespace: string + clusterName: string +} +export function createInvokeArgoAction(config: Config, logger: Logger) { + return createTemplateAction({ + id: 'workflows:argo:invoke', + description: + 'Append content to the end of the given file, it will create the file if it does not exist.', + schema: { + input: { + type: 'object', + required: ['name', 'namespace', 'clusterName'], + properties: { + name: { + title: 'Name', + description: 'Name of Argo workflow template', + type: 'string', + }, + namespace: { + title: 'Namespace', + description: 'Namespace to run this workflow', + type: 'string', + }, + clusterName: { + title: 'Cluster name', + description: 'Name of Cluster', + type: 'string', + }, + }, + }, + output: { + type: 'object', + properties: { + ID: { + title: 'ID', + type: 'string', + }, + }, + }, + }, + async handler(ctx: ActionContext) { + logger.debug(`Invoked with ${ctx.input}`) + + const c = config.getConfigArray("kubernetes.clusterLocatorMethods") + const cc = c.filter(function(val) { + return val.getString("type") === "config" + }) + logger.info(`found ${cc.length} statically configured clusters`) + + const clusters = new Array(); + // this is shit + cc.forEach(function(conf ) { + const cl = conf.getConfigArray("clusters") + cl.forEach(function(val) { + if (val.getString("name") === ctx.input.clusterName) { + clusters.push(val) + } + }) + }) + if (clusters.length === 0 ) { + throw new Error("Cluster not found") + } + const targetCluster = clusters[0] + 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.CoreV1Api) + logger.info("made client") + try { + const resp = await client.listNamespace() + logger.info(`response: ${resp.body}`) + + } catch (error) { + if (error instanceof HttpError) { + logger.info(`error : ${error.response.statusCode} ${error.response.statusMessage}`) + throw new Error(`Failed to talk to the cluster: ${error.response.statusCode} ${error.response.statusMessage}`) + } + if (error instanceof Error) { + logger.error(`error while talking to cluster: ${error.name} ${error.message}`) + } + throw new Error("Unknown exception was encountered.") + } + } + } + ) +} \ No newline at end of file