merge back to main (#8)

bump backstage version 
add cluster pickers
add spark and argo workflows plugins
This commit is contained in:
Manabu McCloskey 2023-07-24 15:59:28 -07:00 committed by GitHub
parent 383885df21
commit 0061e42d91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 3235 additions and 1814 deletions

3
.gitignore vendored
View file

@ -49,3 +49,6 @@ site
# vscode database functionality support files
*.session.sql
# JetBrains
.idea

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,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",

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';
@ -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) => <ProxiedSignInPage {...props} provider="oauth2Proxy" />,
SignInPage: props => (
<SignInPage
{...props}
auto
provider={{
id: 'keycloak-oidc',
title: 'Keycloak',
message: 'Sign in using Keycloak',
apiRef: keycloakOIDCAuthApiRef,
}}
/>
),
SignInPage: props => (
<SignInPage
{...props}
auto
provider={{
id: 'keycloak-oidc',
title: 'Keycloak',
message: 'Sign in using Keycloak',
apiRef: keycloakOIDCAuthApiRef,
}}
/>
),
},
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
@ -88,7 +93,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

@ -4,26 +4,25 @@ import {
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory,
ApiRef,
BackstageIdentityApi,
configApiRef,
createApiFactory,
createApiRef,
discoveryApiRef,
oauthRequestApiRef,
OpenIdConnectApi,
ProfileInfoApi,
SessionApi,
AnyApiFactory,
ApiRef,
BackstageIdentityApi,
configApiRef,
createApiFactory,
createApiRef,
discoveryApiRef,
oauthRequestApiRef,
OpenIdConnectApi,
ProfileInfoApi,
SessionApi,
} from '@backstage/core-plugin-api';
import {OAuth2} from "@backstage/core-app-api";
import {keycloakOIDCAuthApiRef} from "@internal/plugin-workflows"
import { OAuth2 } from '@backstage/core-app-api';
// export const keycloakOIDCAuthApiRef: ApiRef<
// OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
// > = 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'],
}),
}),
];

View file

@ -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 = (
<EntityTechdocsContent>
@ -121,6 +127,16 @@ const overviewContent = (
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<EntitySwitch>
<EntitySwitch.Case if={e => isArgoWorkflowsAvailable(e)}>
<Grid item md={6}>
<EntityArgoWorkflowsOverviewCard />
</Grid>
<Grid item md={6}>
<EntityArgoWorkflowsTemplateOverviewCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
@ -199,6 +215,17 @@ const websiteEntityPage = (
</EntityLayout>
);
const jobEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/apache-spark" title="Apache Spark">
<ApacheSparkPage />
</EntityLayout.Route>
</EntityLayout>
);
/**
* 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 = (
<EntitySwitch.Case if={isComponentType('website')}>
{websiteEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case if={isComponentType('job')}>
{jobEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
@ -349,9 +379,6 @@ const systemPage = (
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
<EntityLayout.Route path="/workflows" title="Workflows">
<EntityWorkflowsContent />
</EntityLayout.Route>
</EntityLayout>
);

View file

@ -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 (
<>
<h1>{title}</h1>
<h2>In two column layout!!</h2>
<Grid container justifyContent="flex-end">
{properties.slice(0, mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
{properties.slice(mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
</Grid>
{description}
</>
);
};
function CollapsableFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(props: ObjectFieldTemplateProps<T, S, F>) {
const {
registry,
properties,
title,
description,
uiSchema,
required,
schema,
idSchema,
} = props;
const [collapsed, setCollapsed] = useState(false);
const out = (
<div>
{title} hiii{description}
<Button
variant="outlined"
size="small"
style={{
display: 'inline-block',
float: 'right',
fontSize: 'large',
}}
onClick={() => setCollapsed(!collapsed)}
>
Collapse
</Button>
<div>
{collapsed
? null
: properties.map(prop => (
<div key={prop.content.key}>
<Button
variant="outlined"
size="small"
style={{
display: 'inline-block',
float: 'right',
fontSize: 'large',
}}
onClick={() => setCollapsed(!collapsed)}
>
Collapse
</Button>
{prop.content}
</div>
))}
</div>
</div>
);
return out;
// return (
// <>
// {hidden ? null : (
// <div className={classNames}>
// <>
// {!isThisTheTopmostElement() && (
// <Button
// variant="outlined"
// size="small"
// style={{
// display: 'inline-block',
// float: 'right',
// fontSize: 'large',
// }}
// onClick={() => setCollapsed(!collapsed)}
// >
// {collapsed ? (
// <>
// +
// {(errors?.props?.errors ?? []).length ? (
// <span style={{ fontSize: 'small' }}>
// {' '}
// (Contains errors)
// </span>
// ) : null}
// </>
// ) : (
// '-'
// )}
// </Button>
// )}
// {get(schema, 'type', undefined) !== 'object' &&
// get(schema, 'type', undefined) !== 'array' ? (
// <>{label ? `${label}${required ? ' *required' : ''}` : null}</>
// ) : (
// <fieldset className="field field-array field-array-of-object">
// {label ? (
// <legend>{`${label}${required ? '*required' : ''}`}</legend>
// ) : null}
// </fieldset>
// )}
// {!collapsed && (
// <>
// {get(schema, 'type', undefined) !== 'object' &&
// get(schema, 'type', undefined) !== 'array'
// ? description
// : null}
// {children}
// {errors}
// {help}
// </>
// )}
// </>
// </div>
// )}
// </>
// );
}
export const CollapsableField = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'CollapsableField',
component: CollapsableFieldTemplate,
}),
);

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,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 <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>
);
};

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,38 +16,41 @@
"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",
"@backstage/types": "^1.1.0",
"@internal/plugin-argo-workflows-backend-backend": "^0.1.0",
"@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",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"pg": "^8.3.0",
"winston": "^3.2.1"
"winston": "^3.2.1",
"yaml": "^2.3.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

@ -5,7 +5,11 @@ import {
} from '@backstage/plugin-auth-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import {DEFAULT_NAMESPACE, stringifyEntityRef} from "@backstage/catalog-model";
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { JsonArray } from '@backstage/types';
export default async function createPlugin(
env: PluginEnvironment,
@ -26,83 +30,16 @@ export default async function createPlugin(
name: info.result.userinfo.sub,
namespace: DEFAULT_NAMESPACE,
});
console.log(info.result.userinfo.groups)
return ctx.issueToken({
claims: {
sub: userRef, // The user's own identity
ent: [userRef], // A list of identities that the user claims ownership through
sub: userRef,
ent: [userRef],
groups: (info.result.userinfo.groups as JsonArray) || [],
},
});
},
},
}),
},
// providerFactories: {
// ...defaultAuthProviderFactories,
// oauth2Proxy: providers.oauth2Proxy.create({
// signIn: {
// async resolver({ result }, ctx) {
// console.log(result)
// const name = result.getHeader('x-forwarded-preferred-username');
// if (!name) {
// throw new Error('Request did not contain a user');
// }
//
// try {
// // Attempts to sign in existing user
// const signedInUser = await ctx.signInWithCatalogUser({
// entityRef: { name },
// });
//
// return Promise.resolve(signedInUser);
// } catch (e) {
// // Create stub user
// const userEntityRef = stringifyEntityRef({
// kind: 'User',
// name: name,
// namespace: DEFAULT_NAMESPACE,
// });
// return ctx.issueToken({
// claims: {
// sub: userEntityRef,
// ent: [userEntityRef],
// },
// });
// }
// },
// },
// }),
// // This replaces the default GitHub auth provider with a customized one.
// // The `signIn` option enables sign-in for this provider, using the
// // identity resolution logic that's provided in the `resolver` callback.
// //
// // This particular resolver makes all users share a single "guest" identity.
// // It should only be used for testing and trying out Backstage.
// //
// // If you want to use a production ready resolver you can switch to
// // the one that is commented out below, it looks up a user entity in the
// // catalog using the GitHub username of the authenticated user.
// // That resolver requires you to have user entities populated in the catalog,
// // for example using https://backstage.io/docs/integrations/github/org
// //
// // There are other resolvers to choose from, and you can also create
// // your own, see the auth documentation for more details:
// //
// // https://backstage.io/docs/auth/identity-resolver
// // github: providers.github.create({
// // signIn: {
// // resolver(_, ctx) {
// // const userRef = 'user:default/guest'; // Must be a full entity reference
// // return ctx.issueToken({
// // claims: {
// // sub: userRef, // The user's own identity
// // ent: [userRef], // A list of identities that the user claims ownership through
// // },
// // });
// // },
// // // resolver: providers.github.resolvers.usernameMatchingUserEntityName(),
// // },
// // }),
// },
});
}

View file

@ -0,0 +1,182 @@
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { KubeConfig, CustomObjectsApi } from '@kubernetes/client-node';
import YAML from 'yaml';
import { Config } from '@backstage/config';
import { resolveSafeChildPath } from '@backstage/backend-common';
import fs from 'fs-extra';
export const kubernetesApply = (config: Config) => {
return createTemplateAction<{
manifestString?: string;
manifestObject?: any;
manifestPath?: string;
namespaced: boolean;
clusterName: string;
}>({
id: 'cnoe:kubernetes:apply',
schema: {
input: {
type: 'object',
required: ['namespaced'],
properties: {
manifestString: {
type: 'string',
title: 'Manifest',
description: 'The manifest to apply in the cluster',
},
manifestObject: {
type: 'object',
title: 'Manifest',
description: 'The manifest to apply in the cluster',
},
manifestPath: {
type: 'string',
title: 'Path to the manifest file',
description: 'The path to the manifest file',
},
namespaced: {
type: 'boolean',
title: 'Namespaced',
description: 'Whether the API is namespaced or not',
},
clusterName: {
type: 'string',
title: 'Cluster Name',
description: 'The name of the cluster to apply this',
},
},
},
},
async handler(ctx) {
let obj: any;
if (ctx.input.manifestString) {
obj = YAML.parse(ctx.input.manifestString);
} else if (ctx.input.manifestObject) {
obj = ctx.input.manifestObject;
} else {
const filePath = resolveSafeChildPath(
ctx.workspacePath,
ctx.input.manifestPath!,
);
const fileContent = fs.readFileSync(filePath, 'utf8');
obj = YAML.parse(fileContent);
}
const words = obj.apiVersion.split('/');
const group = words[0];
const version = words[1];
// hack. needs fixing to correctly extract the plurals
const plural = `${obj.kind.toLowerCase()}s`;
const targetCluster = getClusterConfig(ctx.input.clusterName!, config);
// hack. needs fixing to get the KubeConfig info from app-config.yaml
const kc = new KubeConfig();
kc.addCluster({
name: targetCluster.getString('name'),
caData: targetCluster.getString('caData'),
server: targetCluster.getString('url'),
skipTLSVerify: targetCluster.getBoolean('skipTLSVerify'),
});
kc.addUser({
name: 'scaffolder-user',
token: targetCluster.getString('serviceAccountToken'),
});
kc.addContext({
cluster: ctx.input.clusterName,
user: 'scaffolder-user',
name: ctx.input.clusterName,
});
kc.setCurrentContext(ctx.input.clusterName);
const client = kc.makeApiClient(CustomObjectsApi);
// Server-side apply.
if (ctx.input.namespaced) {
await client
.patchNamespacedCustomObject(
group,
version,
obj.metadata.namespace,
plural,
obj.metadata.name,
obj,
undefined,
'backstage',
true,
{ headers: { 'Content-Type': 'application/apply-patch+yaml' } },
)
.then(
resp => {
ctx.logger.info(
`Successfully created ${obj.metadata.namespace}/${obj.metadata.name} Application: HTTP ${resp.response.statusCode}`,
);
},
err => {
ctx.logger.error(
`Failed to make PATCH call for ${obj.metadata.namespace}/${
obj.metadata.name
} Application: Body ${JSON.stringify(
err.body,
null,
2,
)} Response ${JSON.stringify(err.response, null, 2)}.`,
);
throw err;
},
);
return;
}
await client
.patchClusterCustomObject(
group,
version,
plural,
obj.metadata.name,
obj,
undefined,
'backstage',
true,
{ headers: { 'Content-Type': 'application/apply-patch+yaml' } },
)
.then(
resp => {
ctx.logger.info(
`Successfully created ${obj.metadata.name} Application: HTTP ${resp.response.statusCode}`,
);
},
err => {
ctx.logger.error(
`Failed to make PATCH call for ${
obj.metadata.name
} Application: Body ${JSON.stringify(
err.body,
null,
2,
)} Response ${JSON.stringify(err.response, null, 2)}.`,
);
throw err;
},
);
},
});
};
function getClusterConfig(name: string, config: Config): Config {
const clusterConfigs = config
.getConfigArray('kubernetes.clusterLocatorMethods')
.filter((val: Config) => {
return val.getString('type') === 'config';
});
const clusters = new Array<Config>();
clusterConfigs.filter((conf: Config) => {
const cluster = conf.getConfigArray('clusters').find((val: Config) => {
return val.getString('name') === name;
});
if (cluster) {
clusters.push(cluster);
}
});
if (clusters.length === 0) {
throw new Error(`Cluster with name ${name} not found`);
}
return clusters[0];
}

View file

@ -4,13 +4,15 @@ import { PluginEnvironment } from '../types';
import { CatalogClient } from '@backstage/catalog-client';
export default async function createPlugin(
env: PluginEnvironment,
env: PluginEnvironment,
): Promise<Router> {
const catalogApi = new CatalogClient({discoveryApi: env.discovery});
const {router} = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
catalogApi,
}).build();
return router;
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
const { router } = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
catalogApi,
permissions: env.permissions,
}).build();
return router;
}

View file

@ -1,9 +1,12 @@
import { CatalogClient } from '@backstage/catalog-client';
import {createBuiltinActions, 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'
import { createInvokeArgoAction } from './workflow-argo';
import {
createZipAction,
createSleepAction,
@ -18,6 +21,7 @@ import {
createYamlJSONataTransformAction,
createJsonJSONataTransformAction,
} from '@roadiehq/scaffolder-backend-module-utils';
import { kubernetesApply } from './kubernetes-apply';
export default async function createPlugin(
env: PluginEnvironment,
@ -34,7 +38,7 @@ export default async function createPlugin(
reader: env.reader,
});
const scaffolderBackendModuleUtils = [
const scaffolderBackendModuleUtils = [
createZipAction(),
createSleepAction(),
createWriteFileAction(),
@ -46,10 +50,15 @@ export default async function createPlugin(
createSerializeJsonAction(),
createJSONataAction(),
createYamlJSONataTransformAction(),
createJsonJSONataTransformAction()
]
const actions = [...builtInActions, ...scaffolderBackendModuleUtils, createInvokeArgoAction(env.config, env.logger)];
createJsonJSONataTransformAction(),
];
const actions = [
...builtInActions,
...scaffolderBackendModuleUtils,
createInvokeArgoAction(env.config, env.logger),
kubernetesApply(env.config),
];
return await createRouter({
actions: actions,

View file

@ -7,6 +7,7 @@ import {HttpError} from "@kubernetes/client-node";
type argoInput = {
namespace: string
clusterName: string
userOIDCToken: string
templateName: string
parameters: parameter[]
wait?: boolean
@ -98,9 +99,14 @@ export function createInvokeArgoAction(config: Config, logger: Logger) {
description: 'Name of Cluster',
type: 'string',
},
userOIDCToken: {
title: 'User\'s OIDC token',
description: "If specified, it will use the provided token to communicate with the Kubernetes cluster",
type: 'string'
},
templateName: {
title: 'Template name',
description: 'Argo Workflows template name',
description: 'Argo Workflows template name to run',
type: 'string',
},
parameters: {
@ -121,7 +127,7 @@ export function createInvokeArgoAction(config: Config, logger: Logger) {
},
wait: {
title: 'Wait for completion',
description: 'specify weather to wait for completion of this workflow',
description: 'specify weather to wait for completion of this workflow.',
type: 'boolean',
}
},
@ -142,22 +148,23 @@ export function createInvokeArgoAction(config: Config, logger: Logger) {
},
async handler(ctx: ActionContext<argoInput>) {
logger.debug(`Invoked with ${JSON.stringify(ctx.input)})`)
logger.info(JSON.stringify(ctx.secrets))
const targetCluster = getClusterConfig(ctx.input.clusterName, config)
const kc = new k8s.KubeConfig()
kc.addCluster({
name: ctx.input.clusterName,
name: targetCluster.getString("name"),
caData: targetCluster.getString("caData"),
server: targetCluster.getString("url"),
skipTLSVerify: targetCluster.getBoolean("skipTLSVerify"),
})
kc.addUser({
name: "admin",
token: targetCluster.getString("serviceAccountToken")
name: "scaffolder-user",
token: ctx.input.userOIDCToken? ctx.input.userOIDCToken : targetCluster.getString("serviceAccountToken")
})
kc.addContext({
cluster: ctx.input.clusterName,
user: "admin",
user: "scaffolder-user",
name: ctx.input.clusterName
})
kc.setCurrentContext(ctx.input.clusterName)
@ -197,21 +204,23 @@ export function createInvokeArgoAction(config: Config, logger: Logger) {
}
function getClusterConfig(name: string, config: Config): Config {
const c = config.getConfigArray("kubernetes.clusterLocatorMethods")
const cc = c.filter(function(val) {
return val.getString("type") === "config"
})
const clusterConfigs = config.getConfigArray("kubernetes.clusterLocatorMethods").filter(
(val: Config) => {
return val.getString('type') === 'config'
}
)
const clusters = new Array<Config>();
// this is shit
cc.forEach(function(conf ) {
const cl = conf.getConfigArray("clusters")
cl.forEach(function(val) {
if (val.getString("name") === name) {
clusters.push(val)
}
clusterConfigs.filter( (conf: Config) => {
const cluster = conf.getConfigArray("clusters").find( (val: Config) => {
return val.getString("name") === name
})
if (cluster) {
clusters.push(cluster)
}
})
if (clusters.length === 0 ) {
throw new Error(`Cluster with name ${name} not found`)
}

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",

4140
yarn.lock

File diff suppressed because it is too large Load diff