Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scaffolder auth hook and template #235

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .scripts/humanitec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function* cloneEnvironment({ newEnvName }) {
body: JSON.stringify({
active: true,
artefacts_filter: [artefactsFilter],
match_ref: `refs/heads/${newEnvName}/merge`,
match_ref: `refs/heads/${newEnvName.startsWith('pr') ? newEnvName.slice(2) : newEnvName}/merge`,
type: 'update',
}),
});
Expand Down
8 changes: 6 additions & 2 deletions app-config.production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ catalog:
locations:
- type: url
target: https://github.com/thefrontside/playhouse/blob/main/catalog-info.yaml
- type: url
target: https://github.com/thefrontside/playhouse/blob/main/templates/standard-microservice/template.yaml
- type: file
target: ../../templates/standard-microservice/template.yaml
rules:
- allow: [Template]
- type: file
target: ../../templates/echo-repo-with-auth/template.yaml
rules:
- allow: [Template]

Expand Down
4 changes: 4 additions & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ catalog:
target: ../../templates/standard-microservice/template.yaml
rules:
- allow: [Template]
- type: file
target: ../../templates/echo-repo-with-auth/template.yaml
rules:
- allow: [Template]

humanitec:
orgId: the-frontside-software-inc
Expand Down
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"@backstage/plugin-search-react": "^1.4.0",
"@backstage/plugin-tech-radar": "^0.6.0",
"@backstage/plugin-techdocs": "^1.4.3",
"@backstage/plugin-techdocs-react": "^1.1.2",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.0.9",
"@backstage/plugin-techdocs-react": "^1.1.2",
"@backstage/plugin-user-settings": "^0.6.2",
"@backstage/theme": "^0.2.16",
"@frontside/backstage-plugin-effection-inspector": "^0.1.5",
Expand Down
10 changes: 9 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from '@backstage/plugin-catalog-import';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
import { allFieldExtensions } from './scaffolder';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import {
Expand Down Expand Up @@ -105,7 +107,13 @@ const routes = (
path="/create/legacy"
element={<ScaffolderPage groups={[]} />}
/>
<Route path="/create" element={<NextScaffolderPage FormProps={{ noHtml5Validate: true }} />} />
<Route path="/create" element={<NextScaffolderPage FormProps={{ noHtml5Validate: true }} />} >
<ScaffolderFieldExtensions>
{allFieldExtensions.map(Component => (
<Component />
))}
</ScaffolderFieldExtensions>
</Route>
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route
path="/tech-radar"
Expand Down
52 changes: 52 additions & 0 deletions packages/app/src/scaffolder/GithubAuth/GithubAuthExtension.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable import/no-extraneous-dependencies */
import React, { useMemo } from 'react';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';

import { useAuth, RequestUserCredentials } from 'scaffolder-frontend-auth';

export const GithubAuth = (props: FieldExtensionComponentProps<string>) => {
const { uiSchema, onChange, rawErrors } = props;
const uiOptions = useMemo(() => uiSchema?.['ui:options'] ?? {}, [uiSchema]);

const requestUserCredentials =
uiOptions?.requestUserCredentials &&
typeof uiOptions?.requestUserCredentials === 'object'
? (uiOptions?.requestUserCredentials as RequestUserCredentials)
: undefined;

const token = useAuth({
url: 'https://github.com',
requestUserCredentials,
});

return (
<FormControl margin="normal" required error={rawErrors?.length > 0}>
<InputLabel htmlFor="ownerWithRepo">owner/repo</InputLabel>
<Input
id="ownerWithRepo"
aria-describedby="ownerRepoField"
onChange={e => onChange(e.target?.value)}
/>
<FormHelperText id="ownerRepoField">
{`The owner/repo combination to read metadata, e.g. thefrontside/playhouse${
token ? `, using the token ending with ${token?.slice(-5)}` : ''
}`}
</FormHelperText>
</FormControl>
);
};

export const validateOwnerRepoCombination = (
value: string,
validation: { addError: (arg0: string) => void },
) => {
const parts = value?.split('/');

if (parts?.length !== 2) {
validation.addError(`Needs an owner/project format.`);
}
};
23 changes: 23 additions & 0 deletions packages/app/src/scaffolder/GithubAuth/extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts

/*
This is where the magic happens and creates the custom field extension.

Note that if you're writing extensions part of a separate plugin,
then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.
*/

import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
import {
GithubAuth,
validateOwnerRepoCombination,
} from './GithubAuthExtension';

export const GithubAuthFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'GithubAuth',
component: GithubAuth,
validation: validateOwnerRepoCombination,
}),
);
3 changes: 3 additions & 0 deletions packages/app/src/scaffolder/GithubAuth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// packages/app/src/scaffolder/ValidateKebabCase/index.ts

export { GithubAuthFieldExtension } from './extensions';
79 changes: 79 additions & 0 deletions packages/app/src/scaffolder/GithubQuery/GithubQueryExtension.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* eslint-disable import/no-extraneous-dependencies */
import React, { useCallback, useMemo, useState } from 'react';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';

import Autocomplete from '@material-ui/lab/Autocomplete';
import { TextField } from '@material-ui/core';
import {
useGithubApi,
RequestUserCredentials,
} from 'scaffolder-frontend-auth';

export const GithubQuery = (props: FieldExtensionComponentProps<string>) => {
const { uiSchema, onChange, rawErrors, formData, required } = props;
const [ownerInput, setOwnerInput] = useState<string>('thefrontside');
const [owner, setOwner] = useState<string>('thefrontside');
const uiOptions = useMemo(() => uiSchema?.['ui:options'] ?? {}, [uiSchema]);

const requestUserCredentials =
uiOptions?.requestUserCredentials &&
typeof uiOptions?.requestUserCredentials === 'object'
? (uiOptions?.requestUserCredentials as RequestUserCredentials)
: undefined;

const { value, loading } = useGithubApi({
requestUserCredentials,
queryUrl: `orgs/${owner}/repos`,
});

const onSelect = useCallback(
(_: any, selectValue: string | null) => {
onChange(selectValue ?? undefined);
},
[onChange],
);

return (
<>
<FormControl margin="normal" required error={rawErrors?.length > 0}>
<InputLabel htmlFor="owner">organization</InputLabel>
<Input
id="owner"
aria-describedby="ownerField"
value={ownerInput}
onChange={e => setOwnerInput(e.target?.value)}
onBlur={() => setOwner(ownerInput)}
/>
<FormHelperText id="ownerField">
The owner to query a list of repositories
</FormHelperText>
</FormControl>
<FormControl margin="normal" error={rawErrors?.length > 0 && !formData}>
<Autocomplete
value={(formData as string) || ''}
loading={loading}
onChange={onSelect}
options={value?.map((repo: { name: string }) => repo.name) ?? []}
autoSelect
renderInput={params => (
<TextField
{...params}
margin="dense"
FormHelperTextProps={{
margin: 'dense',
style: { marginLeft: 0 },
}}
variant="outlined"
required={required}
InputProps={params.InputProps}
/>
)}
/>
</FormControl>
</>
);
};
19 changes: 19 additions & 0 deletions packages/app/src/scaffolder/GithubQuery/extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts

/*
This is where the magic happens and creates the custom field extension.

Note that if you're writing extensions part of a separate plugin,
then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.
*/

import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
import { GithubQuery } from './GithubQueryExtension';

export const GithubQueryFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'GithubQuery',
component: GithubQuery,
}),
);
3 changes: 3 additions & 0 deletions packages/app/src/scaffolder/GithubQuery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// packages/app/src/scaffolder/ValidateKebabCase/index.ts

export { GithubQueryFieldExtension } from './extensions';
7 changes: 7 additions & 0 deletions packages/app/src/scaffolder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GithubAuthFieldExtension } from './GithubAuth';
import { GithubQueryFieldExtension } from './GithubQuery';

export const allFieldExtensions = [
GithubAuthFieldExtension,
GithubQueryFieldExtension,
];
1 change: 1 addition & 0 deletions plugins/scaffolder-frontend-auth/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
7 changes: 7 additions & 0 deletions plugins/scaffolder-frontend-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# scaffolder-frontend-auth

_This package was created through the Backstage CLI_.

## About

This plugin is not currently published, and intended as an experimentation in hooks to grab a token and use it in a fetch call within a scaffolder workflow.
41 changes: 41 additions & 0 deletions plugins/scaffolder-frontend-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "scaffolder-frontend-auth",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"files": [
"dist"
],
"backstage": {
"role": "web-library"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-plugin-api": "^1.3.0",
"@backstage/integration-react": "^1.1.9",
"@backstage/plugin-scaffolder-react": "^1.0.1",
"react-use": "^17.4.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.22.1",
"@testing-library/jest-dom": "^5.10.1"
}
}
46 changes: 46 additions & 0 deletions plugins/scaffolder-frontend-auth/src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from 'react';
import { useApi } from '@backstage/core-plugin-api';
import { scmAuthApiRef } from '@backstage/integration-react';
import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react';
import useDebounce from 'react-use/lib/useDebounce';

export type RequestUserCredentials = {
additionalScopes: Record<string, string[]>;
secretsKey: string;
};

export const useAuth = ({
requestUserCredentials,
url,
}: {
requestUserCredentials?: RequestUserCredentials;
url?: string;
} = {}) => {
const schAuthApi = useApi(scmAuthApiRef);
const { setSecrets } = useTemplateSecrets();
const [localToken, setToken] = useState<string | undefined>();

useDebounce(
async () => {
if (!requestUserCredentials || !url) return;

const { token } = await schAuthApi.getCredentials({
url,
additionalScope: {
repoWrite: true,
...(requestUserCredentials?.additionalScopes
? { customScopes: requestUserCredentials.additionalScopes }
: {}),
},
});

if (requestUserCredentials?.secretsKey)
setSecrets({ [requestUserCredentials.secretsKey]: token });
setToken(token);
},
500,
[localToken, requestUserCredentials],
);

return localToken ? localToken : undefined;
};
Loading