From 0061e42d915d7801dafc0d53ebe073ddd85d2714 Mon Sep 17 00:00:00 2001 From: Manabu McCloskey Date: Mon, 24 Jul 2023 15:59:28 -0700 Subject: [PATCH] merge back to main (#8) bump backstage version add cluster pickers add spark and argo workflows plugins --- .gitignore | 3 + backstage.json | 2 +- package.json | 2 +- packages/app/package.json | 59 +- packages/app/src/App.tsx | 41 +- packages/app/src/apis.ts | 57 +- .../app/src/components/catalog/EntityPage.tsx | 35 +- .../collapsableFieldTemplate.tsx | 163 + .../src/scaffolder/credentials/extensions.ts | 14 + .../scaffolder/credentials/getOIDCToken.tsx | 102 + .../app/src/scaffolder/credentials/index.ts | 1 + .../app/src/scaffolder/credentials/schema.ts | 20 + packages/backend/package.json | 47 +- packages/backend/src/plugins/auth.ts | 79 +- .../backend/src/plugins/kubernetes-apply.ts | 182 + packages/backend/src/plugins/kubernetes.ts | 18 +- packages/backend/src/plugins/scaffolder.ts | 23 +- packages/backend/src/plugins/workflow-argo.ts | 45 +- plugins/workflows/package.json | 16 +- yarn.lock | 4140 ++++++++++------- 20 files changed, 3235 insertions(+), 1814 deletions(-) create mode 100644 packages/app/src/scaffolder/collapsableFieldTemplate/collapsableFieldTemplate.tsx create mode 100644 packages/app/src/scaffolder/credentials/extensions.ts create mode 100644 packages/app/src/scaffolder/credentials/getOIDCToken.tsx create mode 100644 packages/app/src/scaffolder/credentials/index.ts create mode 100644 packages/app/src/scaffolder/credentials/schema.ts create mode 100644 packages/backend/src/plugins/kubernetes-apply.ts diff --git a/.gitignore b/.gitignore index d452ac2..e90c93e 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ site # vscode database functionality support files *.session.sql + +# JetBrains +.idea diff --git a/backstage.json b/backstage.json index bd61f69..8c10389 100644 --- a/backstage.json +++ b/backstage.json @@ -1,3 +1,3 @@ { - "version": "1.12.1" + "version": "1.14.2" } diff --git a/package.json b/package.json index 06ddb90..29d1922 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/app/package.json b/packages/app/package.json index b6b7764..f536fdb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -18,35 +18,40 @@ "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", + "@cnoe-io/plugin-apache-spark": "file:/var/folders/b7/h6wzrfwn6l30pn3fk5j2794dcy0vlz/T/tmp-26390-66rLxROMRq6K", + "@cnoe-io/plugin-argo-workflows": "file:/var/folders/b7/h6wzrfwn6l30pn3fk5j2794dcy0vlz/T/tmp-32426-R2tjIfGLJy55", "@internal/plugin-workflows": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", + "@rjsf/core": "^5.8.1", + "@rjsf/utils": "^5.8.1", "history": "^5.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -54,7 +59,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", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 309df30..4d74dd8 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -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'; @@ -22,35 +23,39 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; -import {apis} from './apis'; -import {keycloakOIDCAuthApiRef} from "@internal/plugin-workflows" +import { apis, keycloakOIDCAuthApiRef } from './apis'; import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; -import {AlertDisplay, OAuthRequestDialog, SignInPage} from '@backstage/core-components'; +import { + AlertDisplay, + OAuthRequestDialog, + SignInPage, +} from '@backstage/core-components'; import { createApp } from '@backstage/app-defaults'; 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, components: { // SignInPage: (props) => , - SignInPage: props => ( - - ), + SignInPage: props => ( + + ), }, bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { @@ -88,7 +93,11 @@ const routes = ( - } /> + }> + + + + } /> = createApiRef({ -// id: 'auth.keycloak-oidc-provider', -// }); +export const keycloakOIDCAuthApiRef: ApiRef< + OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'auth.keycloak-oidc-provider', +}); export const apis: AnyApiFactory[] = [ createApiFactory({ api: scmIntegrationsApiRef, @@ -39,16 +38,16 @@ export const apis: AnyApiFactory[] = [ configApi: configApiRef, }, factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'keycloak-oidc', - title: 'Keycloak OIDC', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - defaultScopes: ['openid', 'profile', 'email', 'groups'], - }), + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'keycloak-oidc', + title: 'Keycloak OIDC', + icon: () => null, + }, + environment: configApi.getOptionalString('auth.environment'), + defaultScopes: ['openid', 'profile', 'email', 'groups'], + }), }), ]; diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index f7e3765..1ce9e7b 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -58,7 +58,13 @@ import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { EntityKubernetesContent } from '@backstage/plugin-kubernetes'; -import {EntityWorkflowsContent} from '@internal/plugin-workflows' +import { + EntityArgoWorkflowsOverviewCard, + EntityArgoWorkflowsTemplateOverviewCard, + isArgoWorkflowsAvailable, +} from '@cnoe-io/plugin-argo-workflows'; + +import { ApacheSparkPage } from '@cnoe-io/plugin-apache-spark'; const techdocsContent = ( @@ -121,6 +127,16 @@ const overviewContent = ( + + isArgoWorkflowsAvailable(e)}> + + + + + + + + @@ -199,6 +215,17 @@ const websiteEntityPage = ( ); +const jobEntityPage = ( + + + {overviewContent} + + + + + +); + /** * NOTE: This page is designed to work on small screens such as mobile devices. * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`, @@ -227,6 +254,9 @@ const componentPage = ( {websiteEntityPage} + + {jobEntityPage} + {defaultEntityPage} @@ -349,9 +379,6 @@ const systemPage = ( - - - ); diff --git a/packages/app/src/scaffolder/collapsableFieldTemplate/collapsableFieldTemplate.tsx b/packages/app/src/scaffolder/collapsableFieldTemplate/collapsableFieldTemplate.tsx new file mode 100644 index 0000000..6f0344a --- /dev/null +++ b/packages/app/src/scaffolder/collapsableFieldTemplate/collapsableFieldTemplate.tsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; +import { + createScaffolderLayout, + LayoutTemplate, +} from '@backstage/plugin-scaffolder-react'; +import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { Button, Grid } from '@material-ui/core'; +import { + ObjectFieldTemplatePropertyType, + ObjectFieldTemplateProps, + StrictRJSFSchema, + FormContextType, + RJSFSchema, + titleId, + getTemplate, + getUiOptions, +} from '@rjsf/utils'; + +const TwoColumn: LayoutTemplate = ({ properties, description, title }) => { + const mid = Math.ceil(properties.length / 2); + + return ( + <> +

{title}

+

In two column layout!!

+ + {properties.slice(0, mid).map(prop => ( + + {prop.content} + + ))} + {properties.slice(mid).map(prop => ( + + {prop.content} + + ))} + + {description} + + ); +}; + +function CollapsableFieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ObjectFieldTemplateProps) { + const { + registry, + properties, + title, + description, + uiSchema, + required, + schema, + idSchema, + } = props; + const [collapsed, setCollapsed] = useState(false); + + const out = ( +
+ {title} hiii{description} + +
+ {collapsed + ? null + : properties.map(prop => ( +
+ + {prop.content} +
+ ))} +
+
+ ); + return out; + // return ( + // <> + // {hidden ? null : ( + //
+ // <> + // {!isThisTheTopmostElement() && ( + // + // )} + // {get(schema, 'type', undefined) !== 'object' && + // get(schema, 'type', undefined) !== 'array' ? ( + // <>{label ? `${label}${required ? ' *required' : ''}` : null} + // ) : ( + //
+ // {label ? ( + // {`${label}${required ? '*required' : ''}`} + // ) : null} + //
+ // )} + // {!collapsed && ( + // <> + // {get(schema, 'type', undefined) !== 'object' && + // get(schema, 'type', undefined) !== 'array' + // ? description + // : null} + // {children} + // {errors} + // {help} + // + // )} + // + //
+ // )} + // + // ); +} + +export const CollapsableField = scaffolderPlugin.provide( + createScaffolderLayout({ + name: 'CollapsableField', + component: CollapsableFieldTemplate, + }), +); diff --git a/packages/app/src/scaffolder/credentials/extensions.ts b/packages/app/src/scaffolder/credentials/extensions.ts new file mode 100644 index 0000000..8313c7b --- /dev/null +++ b/packages/app/src/scaffolder/credentials/extensions.ts @@ -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, + } + ) +) diff --git a/packages/app/src/scaffolder/credentials/getOIDCToken.tsx b/packages/app/src/scaffolder/credentials/getOIDCToken.tsx new file mode 100644 index 0000000..9614852 --- /dev/null +++ b/packages/app/src/scaffolder/credentials/getOIDCToken.tsx @@ -0,0 +1,102 @@ +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 ; + } + + return ( + 0} + > +