From 13226881189232e2b3e77ecd0bc06f03645a7cfe Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 9 Feb 2023 09:09:33 +0100 Subject: [PATCH 01/34] cli: Add admin skeleton command Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/command.ts | 31 ++++++++++++++++++++++ packages/cli/src/commands/admin/index.ts | 17 ++++++++++++ packages/cli/src/commands/index.ts | 8 ++++++ 3 files changed, 56 insertions(+) create mode 100644 packages/cli/src/commands/admin/command.ts create mode 100644 packages/cli/src/commands/admin/index.ts diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts new file mode 100644 index 0000000000..3539ddc40c --- /dev/null +++ b/packages/cli/src/commands/admin/command.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { Task } from '../../lib/tasks'; + +export async function command(): Promise { + const answers = await inquirer.prompt<{ github: boolean }>([ + { + type: 'confirm', + name: 'github', + message: chalk.blue('Do you want to set up GitHub Authentication?'), + }, + ]); + + Task.log(answers.github ? 'Ok!' : 'Maybe next time'); +} diff --git a/packages/cli/src/commands/admin/index.ts b/packages/cli/src/commands/admin/index.ts new file mode 100644 index 0000000000..4c23ea6e48 --- /dev/null +++ b/packages/cli/src/commands/admin/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ddec5c6a4a..2baf1aac9b 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -25,6 +25,13 @@ const configOption = [ Array(), ] as const; +export function registerAdminCommand(program: Command) { + program + .command('admin') + .description('Get help setting up your Backstage App.') + .action(lazy(() => import('./admin').then(m => m.command))); +} + export function registerRepoCommand(program: Command) { const command = program .command('repo [command]') @@ -360,6 +367,7 @@ export function registerCommands(program: Command) { registerRepoCommand(program); registerScriptCommand(program); registerMigrateCommand(program); + registerAdminCommand(program); program .command('versions:bump') From 7af6c36adfea09e08dbb9a95c307e5c2c272e7e8 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 9 Feb 2023 14:33:17 +0100 Subject: [PATCH 02/34] cli: Expand admin command to setup basic authenticaton for gihub Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/command.ts | 54 ++++++++++- packages/cli/src/commands/admin/file.ts | 48 ++++++++++ packages/cli/src/commands/admin/github.ts | 104 +++++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/commands/admin/file.ts create mode 100644 packages/cli/src/commands/admin/github.ts diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts index 3539ddc40c..5e8256c14f 100644 --- a/packages/cli/src/commands/admin/command.ts +++ b/packages/cli/src/commands/admin/command.ts @@ -17,15 +17,61 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../lib/tasks'; +import { github } from './github'; +import { updateConfigFile, updateEnvFile } from './file'; export async function command(): Promise { - const answers = await inquirer.prompt<{ github: boolean }>([ + const answers = await inquirer.prompt<{ + shouldSetupAuth: boolean; + useEnvForSecrets?: boolean; + provider?: string; + }>([ { type: 'confirm', - name: 'github', - message: chalk.blue('Do you want to set up GitHub Authentication?'), + name: 'shouldSetupAuth', + message: chalk.blue( + 'Do you want to set up Authentication for this project?', + ), + }, + { + type: 'confirm', + name: 'useEnvForSecrets', + message: + 'Would you like to store sensitive configuration details such as secrets as environment variables? (recommended)', + when: ({ shouldSetupAuth }) => shouldSetupAuth, + }, + { + type: 'list', + name: 'provider', + message: 'Please select a provider:', + choices: ['github'], + when: ({ shouldSetupAuth }) => shouldSetupAuth, }, ]); - Task.log(answers.github ? 'Ok!' : 'Maybe next time'); + if (!answers.shouldSetupAuth) { + // TODO(eide): Can we add a Task.warning() method? + console.log( + chalk.yellow( + 'If you change your mind, feel free to re-run this command.', + ), + ); + process.exit(1); + } + + switch (answers.provider) { + case 'github': { + const { useEnvForSecrets } = answers; + const config = await github(useEnvForSecrets); + await updateConfigFile(config); + if (useEnvForSecrets) { + await updateEnvFile(config); + } + break; + } + default: + throw new Error(`Provider ${answers.provider} not implemented yet.`); + } + + Task.log(`Done setting up ${answers.provider}!`); } diff --git a/packages/cli/src/commands/admin/file.ts b/packages/cli/src/commands/admin/file.ts new file mode 100644 index 0000000000..52a55c3776 --- /dev/null +++ b/packages/cli/src/commands/admin/file.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import * as fs from 'fs-extra'; +import yaml from 'yaml'; +import { findPaths } from '@backstage/cli-common'; +import { GithubAuthConfig } from './github'; + +/* eslint-disable-next-line no-restricted-syntax */ +const { targetRoot } = findPaths(__dirname); +const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.development.yaml'); +const ENV_CONFIG_FILE = path.join(targetRoot, '.env.development'); + +// export const readAppConfig = async (file: string) => { +// return yaml.parse(await fs.readFile(file, 'utf8')); +// }; + +export const updateConfigFile = async (config: GithubAuthConfig) => { + return await fs.writeFile( + APP_CONFIG_FILE, + yaml.stringify(config, { + indent: 2, + }), + 'utf8', + ); +}; + +export const updateEnvFile = async (config: GithubAuthConfig) => { + const content = ` +AUTH_GITHUB_CLIENT_ID=${config.auth.providers.github.clientId} +AUTH_GITHUB_CLIENT_SECRET=${config.auth.providers.github.clientId}`; + + return await fs.writeFile(ENV_CONFIG_FILE, content, 'utf8'); +}; diff --git a/packages/cli/src/commands/admin/github.ts b/packages/cli/src/commands/admin/github.ts new file mode 100644 index 0000000000..5b359ecb33 --- /dev/null +++ b/packages/cli/src/commands/admin/github.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { Task } from '../../lib/tasks'; + +export type GithubAuthConfig = { + auth: { + providers: { + github: { + clientId: string; + clientSecret: string; + enterpriseInstanceUrl?: string; + }; + }; + }; +}; + +export const github = async ( + useEnvForSecrets?: boolean, +): Promise => { + Task.log(` + To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( + 'https://github.com/settings/developers', + )} + The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend. + + You can find the full documentation page here: ${chalk.blue( + 'https://backstage.io/docs/auth/github/provider', + )} + `); + + const answers = await inquirer.prompt<{ + clientSecret: string; + clientId: string; + hasGithubEnterprise: boolean; + enterpriseInstanceUrl?: string; + }>([ + { + type: 'input', + name: 'clientSecret', + message: 'What is your Client Secret?', + // TODO(eide): Is there another way to validate? + // validate(input) { + // if (/([a-f0-9]{40})/g.test(input)) { + // return true; + // } + + // throw Error('Please provide a valid client secret.'); + // }, + }, + { + type: 'input', + name: 'clientId', + message: 'What is your Client Id?', + }, + { + type: 'confirm', + name: 'hasGithubEnterprise', + message: 'Are you using Github Enterprise?', + }, + { + type: 'input', + name: 'enterpriseInstanceUrl', + message: 'What is your URL for Github Enterprise?', + when: ({ hasGithubEnterprise }) => hasGithubEnterprise, + validate(input: string) { + return Boolean(new URL(input)); + }, + }, + ]); + + return { + auth: { + providers: { + github: { + clientId: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_ID}' + : answers.clientId, + clientSecret: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_SECRET}' + : answers.clientSecret, + ...(answers.hasGithubEnterprise && { + enterpriseInstanceUrl: answers.enterpriseInstanceUrl, + }), + }, + }, + }, + }; +}; From e1754adefa7f34b67d47bfe4b8f504177dbf6540 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 9 Feb 2023 14:38:42 +0100 Subject: [PATCH 03/34] cli: Set admin command to hidden Signed-off-by: Marcus Eide --- packages/cli/src/commands/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 2baf1aac9b..df0689b4b5 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -27,7 +27,7 @@ const configOption = [ export function registerAdminCommand(program: Command) { program - .command('admin') + .command('admin', { hidden: true }) .description('Get help setting up your Backstage App.') .action(lazy(() => import('./admin').then(m => m.command))); } From 29534d24534c5aa37ed3b3b2e27fbee4b8b1d15c Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Thu, 9 Feb 2023 17:37:44 +0100 Subject: [PATCH 04/34] Add gh-auth Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/admin/gh-auth.ts | 30 +++++++++++++++++++ .../GithubCreateAppServer.ts | 5 +++- .../src/commands/create-github-app/index.ts | 1 + 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/commands/admin/gh-auth.ts diff --git a/packages/cli/src/commands/admin/gh-auth.ts b/packages/cli/src/commands/admin/gh-auth.ts new file mode 100644 index 0000000000..da3036d087 --- /dev/null +++ b/packages/cli/src/commands/admin/gh-auth.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import createGithubApp from '../create-github-app'; + +export default async () => { + // TODO: Make the GitHub Org optional + const input = await inquirer.prompt<{ org: string }>([ + { + type: 'input', + name: 'org', + message: chalk.blue('Enter a GitHub Org [required]'), + }, + ]); + await createGithubApp(input.org); +}; diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts index 70b8c0ca53..ca5eb2c02a 100644 --- a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts +++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts @@ -117,8 +117,11 @@ export class GithubCreateAppServer { ...(this.permissions.includes('members') && { members: 'read' }), ...(this.permissions.includes('read') && { contents: 'read' }), ...(this.permissions.includes('write') && { + administration: 'write', contents: 'write', - actions: 'write', + pull_requests: 'write', + issues: 'write', + workflows: 'write', }), }, name: 'Backstage-', diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts index 6b3e732f18..e04e39fdf6 100644 --- a/packages/cli/src/commands/create-github-app/index.ts +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -26,6 +26,7 @@ import openBrowser from 'react-dev-utils/openBrowser'; // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest export default async (org: string) => { + // Why is the Org check not done here? const answers: Answers = await inquirer.prompt({ name: 'appType', type: 'checkbox', From 250fb2af40eb01e96d58235460963a54bb8090a9 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Fri, 10 Feb 2023 13:43:16 +0100 Subject: [PATCH 05/34] Add GHA option to auth flow Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/admin/command.ts | 26 ++- packages/cli/src/commands/admin/gh-auth.ts | 7 +- .../GithubCreateAppServer.ts | 2 +- .../src/commands/create-github-app/index.ts | 160 +++++++++++++----- 4 files changed, 141 insertions(+), 54 deletions(-) diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts index 5e8256c14f..d7ea348571 100644 --- a/packages/cli/src/commands/admin/command.ts +++ b/packages/cli/src/commands/admin/command.ts @@ -19,6 +19,7 @@ import inquirer from 'inquirer'; import { Task } from '../../lib/tasks'; import { github } from './github'; import { updateConfigFile, updateEnvFile } from './file'; +import ghAuth from './gh-auth'; export async function command(): Promise { const answers = await inquirer.prompt<{ @@ -44,7 +45,7 @@ export async function command(): Promise { type: 'list', name: 'provider', message: 'Please select a provider:', - choices: ['github'], + choices: ['GitHub OAuth', 'GitHub App'], when: ({ shouldSetupAuth }) => shouldSetupAuth, }, ]); @@ -59,9 +60,10 @@ export async function command(): Promise { process.exit(1); } + const { useEnvForSecrets } = answers; + switch (answers.provider) { - case 'github': { - const { useEnvForSecrets } = answers; + case 'GitHub OAuth': { const config = await github(useEnvForSecrets); await updateConfigFile(config); if (useEnvForSecrets) { @@ -69,6 +71,24 @@ export async function command(): Promise { } break; } + case 'GitHub App': { + const config = await ghAuth(); + // TODO(tudi2d): Also change integrations + if (useEnvForSecrets) { + await updateEnvFile({ + auth: config.auth, + }); + config.auth.providers.github.clientId = '${AUTH_GITHUB_CLIENT_ID}'; + config.auth.providers.github.clientSecret = + '${AUTH_GITHUB_CLIENT_SECRET}'; + await updateConfigFile({ + auth: config.auth, + }); + } else { + await updateConfigFile({ auth: config.auth }); + } + break; + } default: throw new Error(`Provider ${answers.provider} not implemented yet.`); } diff --git a/packages/cli/src/commands/admin/gh-auth.ts b/packages/cli/src/commands/admin/gh-auth.ts index da3036d087..ee5bbfb2c6 100644 --- a/packages/cli/src/commands/admin/gh-auth.ts +++ b/packages/cli/src/commands/admin/gh-auth.ts @@ -15,10 +15,11 @@ */ import chalk from 'chalk'; import inquirer from 'inquirer'; -import createGithubApp from '../create-github-app'; +import { adminCli } from '../create-github-app'; +// TODO(tudi2d): Wrapper for admin CLI around `create-github-app` - potentially to be removed export default async () => { - // TODO: Make the GitHub Org optional + // TODO(tudi2d): Make the GitHub Org optional const input = await inquirer.prompt<{ org: string }>([ { type: 'input', @@ -26,5 +27,5 @@ export default async () => { message: chalk.blue('Enter a GitHub Org [required]'), }, ]); - await createGithubApp(input.org); + return await adminCli(input.org); }; diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts index ca5eb2c02a..cb2613c691 100644 --- a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts +++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts @@ -33,7 +33,7 @@ const FORM_PAGE = ` `; -type GithubAppConfig = { +export type GithubAppConfig = { appId: number; slug?: string; name?: string; diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts index e04e39fdf6..16cb921ce4 100644 --- a/packages/cli/src/commands/create-github-app/index.ts +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -14,62 +14,26 @@ * limitations under the License. */ -import fs from 'fs-extra'; import chalk from 'chalk'; -import { stringify as stringifyYaml } from 'yaml'; -import inquirer, { Question, Answers } from 'inquirer'; -import { paths } from '../../lib/paths'; -import { GithubCreateAppServer } from './GithubCreateAppServer'; +import fs from 'fs-extra'; +import inquirer, { Question } from 'inquirer'; import fetch from 'node-fetch'; import openBrowser from 'react-dev-utils/openBrowser'; +import { stringify as stringifyYaml } from 'yaml'; +import { GithubAppConfig } from '@backstage/integration'; + +import { paths } from '../../lib/paths'; +import { GithubCreateAppServer } from './GithubCreateAppServer'; + // This is an experimental command that at this point does not support GitHub Enterprise // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest export default async (org: string) => { - // Why is the Org check not done here? - const answers: Answers = await inquirer.prompt({ - name: 'appType', - type: 'checkbox', - message: - 'Select permissions [required] (these can be changed later but then require approvals in all installations)', - choices: [ - { - name: 'Read access to content (required by Software Catalog to ingest data from repositories)', - value: 'read', - checked: true, - }, - { - name: 'Read access to members (required by Software Catalog to ingest GitHub teams)', - value: 'members', - checked: true, - }, - { - name: 'Read and Write to content and actions (required by Software Templates to create new repositories)', - value: 'write', - }, - ], - }); - - if (answers.appType.length === 0) { - console.log(chalk.red('You must select at least one permission')); - process.exit(1); - } - + // TODO(tudi2d): Why is the Org check not done here? + const answers = await permissionAnswers(); await verifyGithubOrg(org); - const { slug, name, ...config } = await GithubCreateAppServer.run({ - org, - permissions: answers.appType, - }); + const { fileName } = await createGHAYaml(org, answers.appType); - const fileName = `github-app-${slug}-credentials.yaml`; - const content = `# Name: ${name}\n${stringifyYaml(config)}`; - await fs.writeFile(paths.resolveTargetRoot(fileName), content); - console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); - console.log( - chalk.yellow( - 'This file contains sensitive credentials, it should not be committed to version control and handled with care!', - ), - ); console.log( "Here's an example on how to update the integrations section in app-config.yaml", ); @@ -83,6 +47,42 @@ integrations: ); }; +/** + * + * @param org string: GitHub Organisation + * @returns Promise: Partial App Config for Admin CLI + */ +export async function adminCli(org: string) { + await verifyGithubOrg(org); + const answers = await permissionAnswers({ + read: true, + members: true, + write: true, + }); + const { fileName, config } = await createGHAYaml(org, answers.appType); + const auth = { + providers: { + github: { + clientId: config.clientId, + clientSecret: config.clientSecret, + }, + }, + }; + const integration = { + github: { + host: 'github.com', + apps: { + $include: `${fileName}`, + }, + }, + }; + + return { + auth, + integration, + }; +} + async function verifyGithubOrg(org: string): Promise { let response; @@ -121,6 +121,7 @@ async function verifyGithubOrg(org: string): Promise { openBrowser('https://github.com/account/organizations/new'); + // TODO(tudi2d): Could we maybe re-run the command rather than exit? console.log( chalk.yellow( 'Please re-run this command when you have created your new organization', @@ -130,3 +131,68 @@ async function verifyGithubOrg(org: string): Promise { process.exit(0); } } + +async function permissionAnswers( + checked: { read: boolean; members: boolean; write: boolean } = { + read: true, + members: true, + write: false, + }, +) { + const answers = await inquirer.prompt<{ appType: string[] }>({ + name: 'appType', + type: 'checkbox', + message: + 'Select permissions [required] (these can be changed later but then require approvals in all installations)', + choices: [ + { + name: 'Read access to content (required by Software Catalog to ingest data from repositories)', + value: 'read', + checked: checked.read, + }, + { + name: 'Read access to members (required by Software Catalog to ingest GitHub teams)', + value: 'members', + checked: checked.members, + }, + { + name: 'Read and Write to content and actions (required by Software Templates to create new repositories)', + value: 'write', + checked: checked.write, + }, + ], + }); + + if (answers.appType.length === 0) { + console.log(chalk.red('You must select at least one permission')); + // TODO(tudi2d): Maybe re-do input rather than exit? + process.exit(1); + } + + return answers; +} + +async function createGHAYaml( + org: string, + permissions: string[], +): Promise<{ fileName: string; config: GithubAppConfig }> { + const { slug, name, ...config } = await GithubCreateAppServer.run({ + org, + permissions, + }); + + const fileName = `github-app-${slug}-credentials.yaml`; + const content = `# Name: ${name}\n${stringifyYaml(config)}`; + await fs.writeFile(paths.resolveTargetRoot(fileName), content); + console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); + console.log( + chalk.yellow( + 'This file contains sensitive credentials, it should not be committed to version control and handled with care!', + ), + ); + + return { + fileName, + config, + }; +} From d844a31fc414bf0b8ff06aa5da9fbfeab3f66cd4 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 10 Feb 2023 14:08:43 +0100 Subject: [PATCH 06/34] cli: Add validation for github credentials Signed-off-by: Marcus Eide --- packages/cli/package.json | 1 + packages/cli/src/commands/admin/command.ts | 5 +- packages/cli/src/commands/admin/github.ts | 83 +++++++++++++++------- yarn.lock | 40 +++-------- 4 files changed, 66 insertions(+), 63 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 8c76c7888b..8a0848a243 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -39,6 +39,7 @@ "@backstage/types": "workspace:^", "@esbuild-kit/cjs-loader": "^2.4.1", "@manypkg/get-packages": "^1.1.3", + "@octokit/oauth-app": "^4.2.0", "@octokit/request": "^6.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@rollup/plugin-commonjs": "^23.0.0", diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts index d7ea348571..eb0ba0b512 100644 --- a/packages/cli/src/commands/admin/command.ts +++ b/packages/cli/src/commands/admin/command.ts @@ -30,9 +30,7 @@ export async function command(): Promise { { type: 'confirm', name: 'shouldSetupAuth', - message: chalk.blue( - 'Do you want to set up Authentication for this project?', - ), + message: 'Do you want to set up Authentication for this project?', }, { type: 'confirm', @@ -51,7 +49,6 @@ export async function command(): Promise { ]); if (!answers.shouldSetupAuth) { - // TODO(eide): Can we add a Task.warning() method? console.log( chalk.yellow( 'If you change your mind, feel free to re-run this command.', diff --git a/packages/cli/src/commands/admin/github.ts b/packages/cli/src/commands/admin/github.ts index 5b359ecb33..0cb900c85d 100644 --- a/packages/cli/src/commands/admin/github.ts +++ b/packages/cli/src/commands/admin/github.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../lib/tasks'; @@ -22,14 +23,41 @@ export type GithubAuthConfig = { auth: { providers: { github: { - clientId: string; - clientSecret: string; - enterpriseInstanceUrl?: string; + development: { + clientId: string; + clientSecret: string; + enterpriseInstanceUrl?: string; + }; }; }; }; }; +const validateCredentials = async (clientId: string, clientSecret: string) => { + try { + const app = new OAuthApp({ + clientId, + clientSecret, + }); + await app.createToken({ + code: '%NOT-VALID-CODE%', + }); + } catch (error) { + // @octokit/request returns a error.response object when a request is rejected. + // We can check it to see what kind of error we received. + + // If error.response is successful we can double-check that the error itself was due to the bad code. + // If that's the case then we can assume that the client id and secret exists as we otherwise would + // have gotten a 400/404. + if ( + error.response.status !== 200 && + error.response.data.error !== 'bad_verification_code' + ) { + throw new Error(`Validating Github Credentials failed.`); + } + } +}; + export const github = async ( useEnvForSecrets?: boolean, ): Promise => { @@ -39,6 +67,11 @@ export const github = async ( )} The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend. + Settings for local development: + ${chalk.cyan(` + Homepage URL: http://localhost:3000 + Authorization callback URL: http://localhost:7007/api/auth/github/handler/frame`)} + You can find the full documentation page here: ${chalk.blue( 'https://backstage.io/docs/auth/github/provider', )} @@ -52,21 +85,15 @@ export const github = async ( }>([ { type: 'input', - name: 'clientSecret', - message: 'What is your Client Secret?', - // TODO(eide): Is there another way to validate? - // validate(input) { - // if (/([a-f0-9]{40})/g.test(input)) { - // return true; - // } - - // throw Error('Please provide a valid client secret.'); - // }, + name: 'clientId', + message: 'What is your Client Id?', + validate: (input: string) => (input.length ? true : false), }, { type: 'input', - name: 'clientId', - message: 'What is your Client Id?', + name: 'clientSecret', + message: 'What is your Client Secret?', + validate: (input: string) => (input.length ? true : false), }, { type: 'confirm', @@ -78,25 +105,27 @@ export const github = async ( name: 'enterpriseInstanceUrl', message: 'What is your URL for Github Enterprise?', when: ({ hasGithubEnterprise }) => hasGithubEnterprise, - validate(input: string) { - return Boolean(new URL(input)); - }, + validate: (input: string) => Boolean(new URL(input)), }, ]); + await validateCredentials(answers.clientId, answers.clientSecret); + return { auth: { providers: { github: { - clientId: useEnvForSecrets - ? '${AUTH_GITHUB_CLIENT_ID}' - : answers.clientId, - clientSecret: useEnvForSecrets - ? '${AUTH_GITHUB_CLIENT_SECRET}' - : answers.clientSecret, - ...(answers.hasGithubEnterprise && { - enterpriseInstanceUrl: answers.enterpriseInstanceUrl, - }), + development: { + clientId: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_ID}' + : answers.clientId, + clientSecret: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_SECRET}' + : answers.clientSecret, + ...(answers.hasGithubEnterprise && { + enterpriseInstanceUrl: answers.enterpriseInstanceUrl, + }), + }, }, }, }, diff --git a/yarn.lock b/yarn.lock index cd69bd547b..f867cadce5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3678,6 +3678,7 @@ __metadata: "@backstage/types": "workspace:^" "@esbuild-kit/cjs-loader": ^2.4.1 "@manypkg/get-packages": ^1.1.3 + "@octokit/oauth-app": ^4.2.0 "@octokit/request": ^6.0.0 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 "@rollup/plugin-commonjs": ^23.0.0 @@ -12534,9 +12535,9 @@ __metadata: languageName: node linkType: hard -"@octokit/oauth-app@npm:^4.0.4, @octokit/oauth-app@npm:^4.0.6": - version: 4.0.6 - resolution: "@octokit/oauth-app@npm:4.0.6" +"@octokit/oauth-app@npm:^4.0.4, @octokit/oauth-app@npm:^4.0.6, @octokit/oauth-app@npm:^4.2.0": + version: 4.2.0 + resolution: "@octokit/oauth-app@npm:4.2.0" dependencies: "@octokit/auth-oauth-app": ^5.0.0 "@octokit/auth-oauth-user": ^2.0.0 @@ -12545,13 +12546,9 @@ __metadata: "@octokit/oauth-authorization-url": ^5.0.0 "@octokit/oauth-methods": ^2.0.0 "@types/aws-lambda": ^8.10.83 - aws-lambda: ^1.0.7 fromentries: ^1.3.1 universal-user-agent: ^6.0.0 - dependenciesMeta: - aws-lambda: - optional: true - checksum: 0d7ceadda4668748a436f52f6c4ae3160a4a5c81a92592c0a6cfc75b0a065ed480880d8861efcabc6ca41b3ccf753d1c1044782a6c8d5fb481d6e8a0acf6d32e + checksum: 6cf5638d27e589daa577653177f2b135c0c346c8f74cc420d9fd0dc0f559a84040073eee147bd555a55bb5756bd0940e6dc4c3a6b3a508a818e3d5bee3837095 languageName: node linkType: hard @@ -17536,20 +17533,6 @@ __metadata: languageName: node linkType: hard -"aws-lambda@npm:^1.0.7": - version: 1.0.7 - resolution: "aws-lambda@npm:1.0.7" - dependencies: - aws-sdk: ^2.814.0 - commander: ^3.0.2 - js-yaml: ^3.14.1 - watchpack: ^2.0.0-beta.10 - bin: - lambda: bin/lambda - checksum: 11316e87b5c4fc36e6bd0495742a3c0ed13befc9527a7b251a58180d141d9afd68b684f37aeb3b53d117d3c2f96747eace31826b683543f1edddc03f392865fd - languageName: node - linkType: hard - "aws-os-connection@npm:^0.2.0": version: 0.2.0 resolution: "aws-os-connection@npm:0.2.0" @@ -17596,7 +17579,7 @@ __metadata: languageName: node linkType: hard -"aws-sdk@npm:^2.1231.0, aws-sdk@npm:^2.814.0, aws-sdk@npm:^2.840.0, aws-sdk@npm:^2.948.0": +"aws-sdk@npm:^2.1231.0, aws-sdk@npm:^2.840.0, aws-sdk@npm:^2.948.0": version: 2.1279.0 resolution: "aws-sdk@npm:2.1279.0" dependencies: @@ -19475,13 +19458,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^3.0.2": - version: 3.0.2 - resolution: "commander@npm:3.0.2" - checksum: 6d14ad030d1904428139487ed31febcb04c1604db2b8d9fae711f60ee6718828dc0e11602249e91c8a97b0e721e9c6d53edbc166bad3cde1596851d59a8f824d - languageName: node - linkType: hard - "commander@npm:^4.0.0": version: 4.1.1 resolution: "commander@npm:4.1.1" @@ -27320,7 +27296,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1, js-yaml@npm:^3.6.1, js-yaml@npm:^3.8.3": +"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.6.1, js-yaml@npm:^3.8.3": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" dependencies: @@ -38736,7 +38712,7 @@ __metadata: languageName: node linkType: hard -"watchpack@npm:^2.0.0-beta.10, watchpack@npm:^2.4.0": +"watchpack@npm:^2.4.0": version: 2.4.0 resolution: "watchpack@npm:2.4.0" dependencies: From 184602db0af82e8b0481502cde16b77e5fcd7222 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 10 Feb 2023 14:09:47 +0100 Subject: [PATCH 07/34] cli: Handle appending config when setting up github auth Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/file.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/admin/file.ts b/packages/cli/src/commands/admin/file.ts index 52a55c3776..6135c53ebf 100644 --- a/packages/cli/src/commands/admin/file.ts +++ b/packages/cli/src/commands/admin/file.ts @@ -22,17 +22,21 @@ import { GithubAuthConfig } from './github'; /* eslint-disable-next-line no-restricted-syntax */ const { targetRoot } = findPaths(__dirname); -const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.development.yaml'); +const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); const ENV_CONFIG_FILE = path.join(targetRoot, '.env.development'); -// export const readAppConfig = async (file: string) => { -// return yaml.parse(await fs.readFile(file, 'utf8')); -// }; +const readConfigFile = async (file: string) => { + return yaml.parse(await fs.readFile(file, 'utf8')); +}; export const updateConfigFile = async (config: GithubAuthConfig) => { + const content = fs.existsSync(APP_CONFIG_FILE) + ? { ...(await readConfigFile(APP_CONFIG_FILE)), ...config } + : config; + return await fs.writeFile( APP_CONFIG_FILE, - yaml.stringify(config, { + yaml.stringify(content, { indent: 2, }), 'utf8', @@ -41,8 +45,12 @@ export const updateConfigFile = async (config: GithubAuthConfig) => { export const updateEnvFile = async (config: GithubAuthConfig) => { const content = ` -AUTH_GITHUB_CLIENT_ID=${config.auth.providers.github.clientId} -AUTH_GITHUB_CLIENT_SECRET=${config.auth.providers.github.clientId}`; +AUTH_GITHUB_CLIENT_ID=${config.auth.providers.github.development.clientId} +AUTH_GITHUB_CLIENT_SECRET=${config.auth.providers.github.development.clientId}`; + + if (fs.existsSync(ENV_CONFIG_FILE)) { + await fs.appendFile(ENV_CONFIG_FILE, content, 'utf8'); + } return await fs.writeFile(ENV_CONFIG_FILE, content, 'utf8'); }; From d9893cb7942bd728a4f7cf56743e09757ef3f478 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 10 Feb 2023 14:10:42 +0100 Subject: [PATCH 08/34] cli: Fix github auth not including environment key Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/command.ts | 5 +++-- packages/cli/src/commands/create-github-app/index.ts | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts index eb0ba0b512..33dd7bd20f 100644 --- a/packages/cli/src/commands/admin/command.ts +++ b/packages/cli/src/commands/admin/command.ts @@ -75,8 +75,9 @@ export async function command(): Promise { await updateEnvFile({ auth: config.auth, }); - config.auth.providers.github.clientId = '${AUTH_GITHUB_CLIENT_ID}'; - config.auth.providers.github.clientSecret = + config.auth.providers.github.development.clientId = + '${AUTH_GITHUB_CLIENT_ID}'; + config.auth.providers.github.development.clientSecret = '${AUTH_GITHUB_CLIENT_SECRET}'; await updateConfigFile({ auth: config.auth, diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts index 16cb921ce4..606e982bd4 100644 --- a/packages/cli/src/commands/create-github-app/index.ts +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -63,8 +63,10 @@ export async function adminCli(org: string) { const auth = { providers: { github: { - clientId: config.clientId, - clientSecret: config.clientSecret, + development: { + clientId: config.clientId, + clientSecret: config.clientSecret, + }, }, }, }; From ba46a192328b7c488c99e9c9d425407f4e32ecc5 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Mon, 13 Feb 2023 13:02:10 +0100 Subject: [PATCH 09/34] cli: Move to folder and split up into different questions Signed-off-by: Marcus Eide --- .../cli/src/commands/admin/{ => auth}/file.ts | 15 ++++- .../admin/{gh-auth.ts => auth/github/app.ts} | 4 +- .../src/commands/admin/auth/github/index.ts | 64 +++++++++++++++++++ .../admin/{github.ts => auth/github/oauth.ts} | 23 +------ packages/cli/src/commands/admin/auth/index.ts | 45 +++++++++++++ packages/cli/src/commands/admin/command.ts | 56 +--------------- 6 files changed, 130 insertions(+), 77 deletions(-) rename packages/cli/src/commands/admin/{ => auth}/file.ts (88%) rename packages/cli/src/commands/admin/{gh-auth.ts => auth/github/app.ts} (92%) create mode 100644 packages/cli/src/commands/admin/auth/github/index.ts rename packages/cli/src/commands/admin/{github.ts => auth/github/oauth.ts} (90%) create mode 100644 packages/cli/src/commands/admin/auth/index.ts diff --git a/packages/cli/src/commands/admin/file.ts b/packages/cli/src/commands/admin/auth/file.ts similarity index 88% rename from packages/cli/src/commands/admin/file.ts rename to packages/cli/src/commands/admin/auth/file.ts index 6135c53ebf..14a8a151f8 100644 --- a/packages/cli/src/commands/admin/file.ts +++ b/packages/cli/src/commands/admin/auth/file.ts @@ -18,7 +18,20 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import yaml from 'yaml'; import { findPaths } from '@backstage/cli-common'; -import { GithubAuthConfig } from './github'; + +export type GithubAuthConfig = { + auth: { + providers: { + github: { + development: { + clientId: string; + clientSecret: string; + enterpriseInstanceUrl?: string; + }; + }; + }; + }; +}; /* eslint-disable-next-line no-restricted-syntax */ const { targetRoot } = findPaths(__dirname); diff --git a/packages/cli/src/commands/admin/gh-auth.ts b/packages/cli/src/commands/admin/auth/github/app.ts similarity index 92% rename from packages/cli/src/commands/admin/gh-auth.ts rename to packages/cli/src/commands/admin/auth/github/app.ts index ee5bbfb2c6..234f8d1e29 100644 --- a/packages/cli/src/commands/admin/gh-auth.ts +++ b/packages/cli/src/commands/admin/auth/github/app.ts @@ -15,10 +15,10 @@ */ import chalk from 'chalk'; import inquirer from 'inquirer'; -import { adminCli } from '../create-github-app'; +import { adminCli } from '../../../create-github-app'; // TODO(tudi2d): Wrapper for admin CLI around `create-github-app` - potentially to be removed -export default async () => { +export const app = async () => { // TODO(tudi2d): Make the GitHub Org optional const input = await inquirer.prompt<{ org: string }>([ { diff --git a/packages/cli/src/commands/admin/auth/github/index.ts b/packages/cli/src/commands/admin/auth/github/index.ts new file mode 100644 index 0000000000..8ea7e63bc9 --- /dev/null +++ b/packages/cli/src/commands/admin/auth/github/index.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import inquirer from 'inquirer'; +import { GithubAuthConfig, updateConfigFile, updateEnvFile } from '../file'; +import { app } from './app'; +import { oauth } from './oauth'; + +export const github = async () => { + const answers = await inquirer.prompt<{ + useEnvForSecrets: boolean; + type: string; + }>([ + { + type: 'confirm', + name: 'useEnvForSecrets', + message: + 'Would you like to store sensitive configuration details such as secrets as environment variables? (recommended)', + }, + { + type: 'list', + name: 'type', + message: 'Do you want to use a Github App or a Github OAuth?', + choices: ['GitHub OAuth', 'GitHub App'], + }, + ]); + + const { type, useEnvForSecrets } = answers; + + switch (type) { + case 'GitHub OAuth': { + const config: GithubAuthConfig = await oauth(useEnvForSecrets); + await updateConfigFile(config); + if (useEnvForSecrets) { + await updateEnvFile(config); + } + break; + } + case 'GitHub App': { + const { auth }: GithubAuthConfig = await app(); + // TODO(tudi2d): Also change integrations + await updateConfigFile({ auth }); + if (useEnvForSecrets) { + await updateEnvFile({ auth }); + } + break; + } + default: + throw new Error(`Unknown selection: ${type}.`); + } +}; diff --git a/packages/cli/src/commands/admin/github.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts similarity index 90% rename from packages/cli/src/commands/admin/github.ts rename to packages/cli/src/commands/admin/auth/github/oauth.ts index 0cb900c85d..4daccdfd8c 100644 --- a/packages/cli/src/commands/admin/github.ts +++ b/packages/cli/src/commands/admin/auth/github/oauth.ts @@ -17,21 +17,7 @@ import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; import inquirer from 'inquirer'; -import { Task } from '../../lib/tasks'; - -export type GithubAuthConfig = { - auth: { - providers: { - github: { - development: { - clientId: string; - clientSecret: string; - enterpriseInstanceUrl?: string; - }; - }; - }; - }; -}; +import { Task } from '../../../../lib/tasks'; const validateCredentials = async (clientId: string, clientSecret: string) => { try { @@ -58,9 +44,7 @@ const validateCredentials = async (clientId: string, clientSecret: string) => { } }; -export const github = async ( - useEnvForSecrets?: boolean, -): Promise => { +export const oauth = async (useEnvForSecrets?: boolean) => { Task.log(` To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( 'https://github.com/settings/developers', @@ -74,8 +58,7 @@ export const github = async ( You can find the full documentation page here: ${chalk.blue( 'https://backstage.io/docs/auth/github/provider', - )} - `); + )}`); const answers = await inquirer.prompt<{ clientSecret: string; diff --git a/packages/cli/src/commands/admin/auth/index.ts b/packages/cli/src/commands/admin/auth/index.ts new file mode 100644 index 0000000000..9af145dcf3 --- /dev/null +++ b/packages/cli/src/commands/admin/auth/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import inquirer from 'inquirer'; +import { Task } from '../../../lib/tasks'; +import { github } from './github'; + +export async function auth(): Promise { + const answers = await inquirer.prompt<{ + provider?: string; + }>([ + { + type: 'list', + name: 'provider', + message: 'Please select a provider:', + choices: ['Github'], + }, + ]); + + const { provider } = answers; + + switch (provider) { + case 'Github': { + await github(); + break; + } + default: + throw new Error(`Provider ${provider} not implemented yet.`); + } + + Task.log(`Done setting up ${provider}!`); +} diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts index 33dd7bd20f..4c49631a80 100644 --- a/packages/cli/src/commands/admin/command.ts +++ b/packages/cli/src/commands/admin/command.ts @@ -16,15 +16,11 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; -import { Task } from '../../lib/tasks'; -import { github } from './github'; -import { updateConfigFile, updateEnvFile } from './file'; -import ghAuth from './gh-auth'; +import { auth } from './auth'; export async function command(): Promise { const answers = await inquirer.prompt<{ shouldSetupAuth: boolean; - useEnvForSecrets?: boolean; provider?: string; }>([ { @@ -32,20 +28,6 @@ export async function command(): Promise { name: 'shouldSetupAuth', message: 'Do you want to set up Authentication for this project?', }, - { - type: 'confirm', - name: 'useEnvForSecrets', - message: - 'Would you like to store sensitive configuration details such as secrets as environment variables? (recommended)', - when: ({ shouldSetupAuth }) => shouldSetupAuth, - }, - { - type: 'list', - name: 'provider', - message: 'Please select a provider:', - choices: ['GitHub OAuth', 'GitHub App'], - when: ({ shouldSetupAuth }) => shouldSetupAuth, - }, ]); if (!answers.shouldSetupAuth) { @@ -57,39 +39,5 @@ export async function command(): Promise { process.exit(1); } - const { useEnvForSecrets } = answers; - - switch (answers.provider) { - case 'GitHub OAuth': { - const config = await github(useEnvForSecrets); - await updateConfigFile(config); - if (useEnvForSecrets) { - await updateEnvFile(config); - } - break; - } - case 'GitHub App': { - const config = await ghAuth(); - // TODO(tudi2d): Also change integrations - if (useEnvForSecrets) { - await updateEnvFile({ - auth: config.auth, - }); - config.auth.providers.github.development.clientId = - '${AUTH_GITHUB_CLIENT_ID}'; - config.auth.providers.github.development.clientSecret = - '${AUTH_GITHUB_CLIENT_SECRET}'; - await updateConfigFile({ - auth: config.auth, - }); - } else { - await updateConfigFile({ auth: config.auth }); - } - break; - } - default: - throw new Error(`Provider ${answers.provider} not implemented yet.`); - } - - Task.log(`Done setting up ${answers.provider}!`); + await auth(); } From e3e0b14ec45125f7689a56e687fe48dfe1c3b3e4 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Mon, 13 Feb 2023 13:40:21 +0100 Subject: [PATCH 10/34] cli: Move file logic to each type of github auth to simplify logic Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/auth/file.ts | 10 +++--- .../cli/src/commands/admin/auth/github/app.ts | 14 ++++++-- .../src/commands/admin/auth/github/index.ts | 13 ++----- .../src/commands/admin/auth/github/oauth.ts | 36 ++++++++++--------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/commands/admin/auth/file.ts b/packages/cli/src/commands/admin/auth/file.ts index 14a8a151f8..baafacc8c6 100644 --- a/packages/cli/src/commands/admin/auth/file.ts +++ b/packages/cli/src/commands/admin/auth/file.ts @@ -19,7 +19,7 @@ import * as fs from 'fs-extra'; import yaml from 'yaml'; import { findPaths } from '@backstage/cli-common'; -export type GithubAuthConfig = { +type GithubAuthConfig = { auth: { providers: { github: { @@ -36,7 +36,7 @@ export type GithubAuthConfig = { /* eslint-disable-next-line no-restricted-syntax */ const { targetRoot } = findPaths(__dirname); const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); -const ENV_CONFIG_FILE = path.join(targetRoot, '.env.development'); +const ENV_CONFIG_FILE = path.join(targetRoot, '.env.local'); const readConfigFile = async (file: string) => { return yaml.parse(await fs.readFile(file, 'utf8')); @@ -56,10 +56,10 @@ export const updateConfigFile = async (config: GithubAuthConfig) => { ); }; -export const updateEnvFile = async (config: GithubAuthConfig) => { +export const updateEnvFile = async (clientId: string, clientSecret: string) => { const content = ` -AUTH_GITHUB_CLIENT_ID=${config.auth.providers.github.development.clientId} -AUTH_GITHUB_CLIENT_SECRET=${config.auth.providers.github.development.clientId}`; +AUTH_GITHUB_CLIENT_ID=${clientId} +AUTH_GITHUB_CLIENT_SECRET=${clientSecret}`; if (fs.existsSync(ENV_CONFIG_FILE)) { await fs.appendFile(ENV_CONFIG_FILE, content, 'utf8'); diff --git a/packages/cli/src/commands/admin/auth/github/app.ts b/packages/cli/src/commands/admin/auth/github/app.ts index 234f8d1e29..8ab067989f 100644 --- a/packages/cli/src/commands/admin/auth/github/app.ts +++ b/packages/cli/src/commands/admin/auth/github/app.ts @@ -16,9 +16,10 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { adminCli } from '../../../create-github-app'; +import { updateConfigFile, updateEnvFile } from '../file'; // TODO(tudi2d): Wrapper for admin CLI around `create-github-app` - potentially to be removed -export const app = async () => { +export const app = async (useEnvForSecrets: boolean) => { // TODO(tudi2d): Make the GitHub Org optional const input = await inquirer.prompt<{ org: string }>([ { @@ -27,5 +28,14 @@ export const app = async () => { message: chalk.blue('Enter a GitHub Org [required]'), }, ]); - return await adminCli(input.org); + + const { auth } = await adminCli(input.org); + + await updateConfigFile({ auth }); + if (useEnvForSecrets) { + await updateEnvFile( + auth.providers.github.development.clientId, + auth.providers.github.development.clientSecret, + ); + } }; diff --git a/packages/cli/src/commands/admin/auth/github/index.ts b/packages/cli/src/commands/admin/auth/github/index.ts index 8ea7e63bc9..f8348d554d 100644 --- a/packages/cli/src/commands/admin/auth/github/index.ts +++ b/packages/cli/src/commands/admin/auth/github/index.ts @@ -15,7 +15,6 @@ */ import inquirer from 'inquirer'; -import { GithubAuthConfig, updateConfigFile, updateEnvFile } from '../file'; import { app } from './app'; import { oauth } from './oauth'; @@ -42,20 +41,12 @@ export const github = async () => { switch (type) { case 'GitHub OAuth': { - const config: GithubAuthConfig = await oauth(useEnvForSecrets); - await updateConfigFile(config); - if (useEnvForSecrets) { - await updateEnvFile(config); - } + await oauth(useEnvForSecrets); break; } case 'GitHub App': { - const { auth }: GithubAuthConfig = await app(); // TODO(tudi2d): Also change integrations - await updateConfigFile({ auth }); - if (useEnvForSecrets) { - await updateEnvFile({ auth }); - } + await app(useEnvForSecrets); break; } default: diff --git a/packages/cli/src/commands/admin/auth/github/oauth.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts index 4daccdfd8c..159592bc2c 100644 --- a/packages/cli/src/commands/admin/auth/github/oauth.ts +++ b/packages/cli/src/commands/admin/auth/github/oauth.ts @@ -18,6 +18,7 @@ import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../../../lib/tasks'; +import { updateConfigFile, updateEnvFile } from '../file'; const validateCredentials = async (clientId: string, clientSecret: string) => { try { @@ -44,7 +45,7 @@ const validateCredentials = async (clientId: string, clientSecret: string) => { } }; -export const oauth = async (useEnvForSecrets?: boolean) => { +export const oauth = async (useEnvForSecrets: boolean) => { Task.log(` To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( 'https://github.com/settings/developers', @@ -94,23 +95,26 @@ export const oauth = async (useEnvForSecrets?: boolean) => { await validateCredentials(answers.clientId, answers.clientSecret); - return { - auth: { - providers: { - github: { - development: { - clientId: useEnvForSecrets - ? '${AUTH_GITHUB_CLIENT_ID}' - : answers.clientId, - clientSecret: useEnvForSecrets - ? '${AUTH_GITHUB_CLIENT_SECRET}' - : answers.clientSecret, - ...(answers.hasGithubEnterprise && { - enterpriseInstanceUrl: answers.enterpriseInstanceUrl, - }), - }, + const auth = { + providers: { + github: { + development: { + clientId: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_ID}' + : answers.clientId, + clientSecret: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_SECRET}' + : answers.clientSecret, + ...(answers.hasGithubEnterprise && { + enterpriseInstanceUrl: answers.enterpriseInstanceUrl, + }), }, }, }, }; + + await updateConfigFile({ auth }); + if (useEnvForSecrets) { + await updateEnvFile(answers.clientId, answers.clientSecret); + } }; From 67c693c6b2b38f7131ae1a4f942f94d89bae8f4c Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 16 Feb 2023 13:50:56 +0100 Subject: [PATCH 11/34] cli: Add ability to patch existing files Signed-off-by: Marcus Eide --- .../admin/auth/{file.ts => config.ts} | 32 ++++---- packages/cli/src/commands/admin/auth/files.ts | 34 +++++++++ .../cli/src/commands/admin/auth/github/app.ts | 11 ++- .../src/commands/admin/auth/github/diffs.ts | 76 +++++++++++++++++++ .../src/commands/admin/auth/github/index.ts | 3 +- .../src/commands/admin/auth/github/oauth.ts | 38 ++++++---- packages/cli/src/commands/admin/auth/patch.ts | 25 ++++++ 7 files changed, 184 insertions(+), 35 deletions(-) rename packages/cli/src/commands/admin/auth/{file.ts => config.ts} (59%) create mode 100644 packages/cli/src/commands/admin/auth/files.ts create mode 100644 packages/cli/src/commands/admin/auth/github/diffs.ts create mode 100644 packages/cli/src/commands/admin/auth/patch.ts diff --git a/packages/cli/src/commands/admin/auth/file.ts b/packages/cli/src/commands/admin/auth/config.ts similarity index 59% rename from packages/cli/src/commands/admin/auth/file.ts rename to packages/cli/src/commands/admin/auth/config.ts index baafacc8c6..d358e8e89d 100644 --- a/packages/cli/src/commands/admin/auth/file.ts +++ b/packages/cli/src/commands/admin/auth/config.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import * as path from 'path'; import * as fs from 'fs-extra'; import yaml from 'yaml'; -import { findPaths } from '@backstage/cli-common'; type GithubAuthConfig = { auth: { @@ -33,22 +31,20 @@ type GithubAuthConfig = { }; }; -/* eslint-disable-next-line no-restricted-syntax */ -const { targetRoot } = findPaths(__dirname); -const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); -const ENV_CONFIG_FILE = path.join(targetRoot, '.env.local'); - -const readConfigFile = async (file: string) => { +const readYaml = async (file: string) => { return yaml.parse(await fs.readFile(file, 'utf8')); }; -export const updateConfigFile = async (config: GithubAuthConfig) => { - const content = fs.existsSync(APP_CONFIG_FILE) - ? { ...(await readConfigFile(APP_CONFIG_FILE)), ...config } +export const updateConfigFile = async ( + file: string, + config: GithubAuthConfig, +) => { + const content = fs.existsSync(file) + ? { ...(await readYaml(file)), ...config } : config; return await fs.writeFile( - APP_CONFIG_FILE, + file, yaml.stringify(content, { indent: 2, }), @@ -56,14 +52,18 @@ export const updateConfigFile = async (config: GithubAuthConfig) => { ); }; -export const updateEnvFile = async (clientId: string, clientSecret: string) => { +export const updateEnvFile = async ( + file: string, + clientId: string, + clientSecret: string, +) => { const content = ` AUTH_GITHUB_CLIENT_ID=${clientId} AUTH_GITHUB_CLIENT_SECRET=${clientSecret}`; - if (fs.existsSync(ENV_CONFIG_FILE)) { - await fs.appendFile(ENV_CONFIG_FILE, content, 'utf8'); + if (fs.existsSync(file)) { + return await fs.appendFile(file, content, 'utf8'); } - return await fs.writeFile(ENV_CONFIG_FILE, content, 'utf8'); + return await fs.writeFile(file, content, 'utf8'); }; diff --git a/packages/cli/src/commands/admin/auth/files.ts b/packages/cli/src/commands/admin/auth/files.ts new file mode 100644 index 0000000000..479cda0472 --- /dev/null +++ b/packages/cli/src/commands/admin/auth/files.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { findPaths } from '@backstage/cli-common'; +import * as path from 'path'; + +/* eslint-disable-next-line no-restricted-syntax */ +const { targetRoot, resolveTargetRoot } = findPaths(__dirname); +export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); +export const ENV_CONFIG_FILE = path.join(targetRoot, '.env.local'); +export const APP_TSX_FILE = path.join( + resolveTargetRoot('packages/app'), + 'src', + 'App.tsx', +); +export const AUTH_BACKEND_PLUGIN_FILE = path.join( + resolveTargetRoot('packages/backend'), + 'src', + 'plugins', + 'auth.ts', +); diff --git a/packages/cli/src/commands/admin/auth/github/app.ts b/packages/cli/src/commands/admin/auth/github/app.ts index 8ab067989f..d78fe2a3d2 100644 --- a/packages/cli/src/commands/admin/auth/github/app.ts +++ b/packages/cli/src/commands/admin/auth/github/app.ts @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import chalk from 'chalk'; + import inquirer from 'inquirer'; import { adminCli } from '../../../create-github-app'; -import { updateConfigFile, updateEnvFile } from '../file'; +import { updateConfigFile, updateEnvFile } from '../config'; +import { APP_CONFIG_FILE, ENV_CONFIG_FILE } from '../files'; // TODO(tudi2d): Wrapper for admin CLI around `create-github-app` - potentially to be removed export const app = async (useEnvForSecrets: boolean) => { @@ -25,15 +26,17 @@ export const app = async (useEnvForSecrets: boolean) => { { type: 'input', name: 'org', - message: chalk.blue('Enter a GitHub Org [required]'), + message: 'Enter a GitHub Org [required]', }, ]); const { auth } = await adminCli(input.org); - await updateConfigFile({ auth }); + // TODO(tudi2d): Also change integrations + await updateConfigFile(APP_CONFIG_FILE, { auth }); if (useEnvForSecrets) { await updateEnvFile( + ENV_CONFIG_FILE, auth.providers.github.development.clientId, auth.providers.github.development.clientSecret, ); diff --git a/packages/cli/src/commands/admin/auth/github/diffs.ts b/packages/cli/src/commands/admin/auth/github/diffs.ts new file mode 100644 index 0000000000..43b6013d4e --- /dev/null +++ b/packages/cli/src/commands/admin/auth/github/diffs.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const addSignInPageDiff = + '@@ -32,11 +32,27 @@\n' + + " import { AppRouter, FlatRoutes } from '@backstage/core-app-api';\n" + + " import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';\n" + + " import { RequirePermission } from '@backstage/plugin-permission-react';\n" + + " import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';\n" + + "+import { githubAuthApiRef } from '@backstage/core-plugin-api';\n" + + "+import { SignInPage } from '@backstage/core-components';\n" + + ' \n' + + ' const app = createApp({\n' + + ' apis,\n' + + '+ components: {\n' + + '+ SignInPage: props => (\n' + + '+ \n' + + '+ ),\n' + + '+ },\n' + + ' bindRoutes({ bind }) {\n' + + ' bind(catalogPlugin.externalRoutes, {\n' + + ' createComponent: scaffolderPlugin.routes.root,\n' + + ' viewTechDoc: techdocsPlugin.routes.docRoot,\n'; + +export const replaceSignInResolverDiff = + '@@ -36,18 +36,18 @@\n' + + ' //\n' + + ' // https://backstage.io/docs/auth/identity-resolver\n' + + ' github: providers.github.create({\n' + + ' signIn: {\n' + + '- resolver(_, ctx) {\n' + + "- const userRef = 'user:default/guest'; // Must be a full entity reference\n" + + '- return ctx.issueToken({\n' + + '- claims: {\n' + + "- sub: userRef, // The user's own identity\n" + + '- ent: [userRef], // A list of identities that the user claims ownership through\n' + + '- },\n' + + '- });\n' + + '- },\n' + + '- // resolver: providers.github.resolvers.usernameMatchingUserEntityName(),\n' + + '+ // resolver(_, ctx) {\n' + + "+ // const userRef = 'user:default/guest'; // Must be a full entity reference\n" + + '+ // return ctx.issueToken({\n' + + '+ // claims: {\n' + + "+ // sub: userRef, // The user's own identity\n" + + '+ // ent: [userRef], // A list of identities that the user claims ownership through\n' + + '+ // },\n' + + '+ // });\n' + + '+ // },\n' + + '+ resolver: providers.github.resolvers.usernameMatchingUserEntityName(),\n' + + ' },\n' + + ' }),\n' + + ' },\n' + + ' });\n'; diff --git a/packages/cli/src/commands/admin/auth/github/index.ts b/packages/cli/src/commands/admin/auth/github/index.ts index f8348d554d..d140b98dd3 100644 --- a/packages/cli/src/commands/admin/auth/github/index.ts +++ b/packages/cli/src/commands/admin/auth/github/index.ts @@ -32,7 +32,7 @@ export const github = async () => { { type: 'list', name: 'type', - message: 'Do you want to use a Github App or a Github OAuth?', + message: 'Do you want to use a Github App or Github OAuth?', choices: ['GitHub OAuth', 'GitHub App'], }, ]); @@ -45,7 +45,6 @@ export const github = async () => { break; } case 'GitHub App': { - // TODO(tudi2d): Also change integrations await app(useEnvForSecrets); break; } diff --git a/packages/cli/src/commands/admin/auth/github/oauth.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts index 159592bc2c..27dbe24f74 100644 --- a/packages/cli/src/commands/admin/auth/github/oauth.ts +++ b/packages/cli/src/commands/admin/auth/github/oauth.ts @@ -18,7 +18,15 @@ import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../../../lib/tasks'; -import { updateConfigFile, updateEnvFile } from '../file'; +import { updateConfigFile, updateEnvFile } from '../config'; +import { + APP_CONFIG_FILE, + APP_TSX_FILE, + AUTH_BACKEND_PLUGIN_FILE, + ENV_CONFIG_FILE, +} from '../files'; +import { patch } from '../patch'; +import { addSignInPageDiff, replaceSignInResolverDiff } from './diffs'; const validateCredentials = async (clientId: string, clientSecret: string) => { try { @@ -64,7 +72,7 @@ export const oauth = async (useEnvForSecrets: boolean) => { const answers = await inquirer.prompt<{ clientSecret: string; clientId: string; - hasGithubEnterprise: boolean; + hasEnterprise: boolean; enterpriseInstanceUrl?: string; }>([ { @@ -81,40 +89,44 @@ export const oauth = async (useEnvForSecrets: boolean) => { }, { type: 'confirm', - name: 'hasGithubEnterprise', + name: 'hasEnterprise', message: 'Are you using Github Enterprise?', }, { type: 'input', name: 'enterpriseInstanceUrl', message: 'What is your URL for Github Enterprise?', - when: ({ hasGithubEnterprise }) => hasGithubEnterprise, + when: ({ hasEnterprise }) => hasEnterprise, validate: (input: string) => Boolean(new URL(input)), }, ]); - await validateCredentials(answers.clientId, answers.clientSecret); + const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = + answers; + + await validateCredentials(clientId, clientSecret); const auth = { providers: { github: { development: { - clientId: useEnvForSecrets - ? '${AUTH_GITHUB_CLIENT_ID}' - : answers.clientId, + clientId: useEnvForSecrets ? '${AUTH_GITHUB_CLIENT_ID}' : clientId, clientSecret: useEnvForSecrets ? '${AUTH_GITHUB_CLIENT_SECRET}' - : answers.clientSecret, - ...(answers.hasGithubEnterprise && { - enterpriseInstanceUrl: answers.enterpriseInstanceUrl, + : clientSecret, + ...(hasEnterprise && { + enterpriseInstanceUrl: enterpriseInstanceUrl, }), }, }, }, }; - await updateConfigFile({ auth }); + await updateConfigFile(APP_CONFIG_FILE, { auth }); if (useEnvForSecrets) { - await updateEnvFile(answers.clientId, answers.clientSecret); + await updateEnvFile(ENV_CONFIG_FILE, clientId, clientSecret); } + + await patch(APP_TSX_FILE, addSignInPageDiff); + await patch(AUTH_BACKEND_PLUGIN_FILE, replaceSignInResolverDiff); }; diff --git a/packages/cli/src/commands/admin/auth/patch.ts b/packages/cli/src/commands/admin/auth/patch.ts new file mode 100644 index 0000000000..2a4eccdb9e --- /dev/null +++ b/packages/cli/src/commands/admin/auth/patch.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs-extra'; +import * as differ from 'diff'; + +export const patch = async (file: string, diff: string) => { + const oldContent = await fs.readFile(file, 'utf8'); + const newContent = differ.applyPatch(oldContent, diff); + + return await fs.writeFile(file, newContent, 'utf8'); +}; From 2f851a3945de3e5bb80ffe15e7ecd939d72d67e3 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 16 Feb 2023 14:05:16 +0100 Subject: [PATCH 12/34] cli: Add minimal instructions for how to try locally Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/cli/src/commands/admin/README.md diff --git a/packages/cli/src/commands/admin/README.md b/packages/cli/src/commands/admin/README.md new file mode 100644 index 0000000000..d2a6e10256 --- /dev/null +++ b/packages/cli/src/commands/admin/README.md @@ -0,0 +1,23 @@ +# admin tool + +## Local dev + +```bash +# Checkout the branch +cd backstage +git fetch +git checkout goa/cli-admin-command + +# Create a new app +cd .. +npx @backstage/create-app +cd new-backstage-app + +# Run the modded cli in the new app +../github/backstage/packages/cli/bin/backstage-cli admin +? Do you want to set up Authentication for this project? (Y/n) +... + +# Try the app with the new changes +yarn dev +``` From 5663de4a3737d51615ceb5d2b5a9802fe21355a0 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 16 Feb 2023 16:18:22 +0100 Subject: [PATCH 13/34] cli: Add user entity to local catalog in auth flow Signed-off-by: Marcus Eide --- .../cli/src/commands/admin/auth/config.ts | 31 +++++++ packages/cli/src/commands/admin/auth/files.ts | 2 + .../src/commands/admin/auth/github/oauth.ts | 86 +++++++++++++------ packages/cli/src/commands/admin/auth/index.ts | 2 + 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/packages/cli/src/commands/admin/auth/config.ts b/packages/cli/src/commands/admin/auth/config.ts index d358e8e89d..659c356360 100644 --- a/packages/cli/src/commands/admin/auth/config.ts +++ b/packages/cli/src/commands/admin/auth/config.ts @@ -29,6 +29,13 @@ type GithubAuthConfig = { }; }; }; + catalog?: { + locations: Array<{ + type: string; + target: string; + rules: Array<{ allow: Array }>; + }>; + }; }; const readYaml = async (file: string) => { @@ -67,3 +74,27 @@ AUTH_GITHUB_CLIENT_SECRET=${clientSecret}`; return await fs.writeFile(file, content, 'utf8'); }; + +export const addUserEntity = async (file: string, username: string) => { + const content = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: username, + annotations: { + 'github.com/user-login': username, + }, + }, + spec: { + memberOf: [], + }, + }; + + return await fs.writeFile( + file, + yaml.stringify(content, { + indent: 2, + }), + 'utf8', + ); +}; diff --git a/packages/cli/src/commands/admin/auth/files.ts b/packages/cli/src/commands/admin/auth/files.ts index 479cda0472..ad9b53a894 100644 --- a/packages/cli/src/commands/admin/auth/files.ts +++ b/packages/cli/src/commands/admin/auth/files.ts @@ -21,6 +21,8 @@ import * as path from 'path'; const { targetRoot, resolveTargetRoot } = findPaths(__dirname); export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); export const ENV_CONFIG_FILE = path.join(targetRoot, '.env.local'); +export const USER_ENTITY_FILE = path.join(targetRoot, 'user-info.yaml'); + export const APP_TSX_FILE = path.join( resolveTargetRoot('packages/app'), 'src', diff --git a/packages/cli/src/commands/admin/auth/github/oauth.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts index 27dbe24f74..64abf520cf 100644 --- a/packages/cli/src/commands/admin/auth/github/oauth.ts +++ b/packages/cli/src/commands/admin/auth/github/oauth.ts @@ -17,13 +17,15 @@ import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; import inquirer from 'inquirer'; +import fetch from 'node-fetch'; import { Task } from '../../../../lib/tasks'; -import { updateConfigFile, updateEnvFile } from '../config'; +import { addUserEntity, updateConfigFile, updateEnvFile } from '../config'; import { APP_CONFIG_FILE, APP_TSX_FILE, AUTH_BACKEND_PLUGIN_FILE, ENV_CONFIG_FILE, + USER_ENTITY_FILE, } from '../files'; import { patch } from '../patch'; import { addSignInPageDiff, replaceSignInResolverDiff } from './diffs'; @@ -53,6 +55,46 @@ const validateCredentials = async (clientId: string, clientSecret: string) => { } }; +const getConfig = (answers: Answers, useEnvForSecrets: boolean) => { + const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = + answers; + + return { + auth: { + providers: { + github: { + development: { + clientId: useEnvForSecrets ? '${AUTH_GITHUB_CLIENT_ID}' : clientId, + clientSecret: useEnvForSecrets + ? '${AUTH_GITHUB_CLIENT_SECRET}' + : clientSecret, + ...(hasEnterprise && { + enterpriseInstanceUrl: enterpriseInstanceUrl, + }), + }, + }, + }, + }, + catalog: { + locations: [ + { + type: 'file', + target: '../../user-info.yaml', + rules: [{ allow: ['User'] }], + }, + ], + }, + }; +}; + +type Answers = { + username: string; + clientSecret: string; + clientId: string; + hasEnterprise: boolean; + enterpriseInstanceUrl?: string; +}; + export const oauth = async (useEnvForSecrets: boolean) => { Task.log(` To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( @@ -69,12 +111,19 @@ export const oauth = async (useEnvForSecrets: boolean) => { 'https://backstage.io/docs/auth/github/provider', )}`); - const answers = await inquirer.prompt<{ - clientSecret: string; - clientId: string; - hasEnterprise: boolean; - enterpriseInstanceUrl?: string; - }>([ + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: 'What is your Github username?', + validate: async (input: string) => { + const response = await fetch(`https://api.github.com/users/${input}`); + if (!response.ok) { + return chalk.red('Unknown user. Please try again.'); + } + return true; + }, + }, { type: 'input', name: 'clientId', @@ -101,32 +150,19 @@ export const oauth = async (useEnvForSecrets: boolean) => { }, ]); - const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = - answers; + const { username, clientId, clientSecret } = answers; await validateCredentials(clientId, clientSecret); - const auth = { - providers: { - github: { - development: { - clientId: useEnvForSecrets ? '${AUTH_GITHUB_CLIENT_ID}' : clientId, - clientSecret: useEnvForSecrets - ? '${AUTH_GITHUB_CLIENT_SECRET}' - : clientSecret, - ...(hasEnterprise && { - enterpriseInstanceUrl: enterpriseInstanceUrl, - }), - }, - }, - }, - }; + const config = getConfig(answers, useEnvForSecrets); + await updateConfigFile(APP_CONFIG_FILE, config); - await updateConfigFile(APP_CONFIG_FILE, { auth }); if (useEnvForSecrets) { await updateEnvFile(ENV_CONFIG_FILE, clientId, clientSecret); } + await addUserEntity(USER_ENTITY_FILE, username); + await patch(APP_TSX_FILE, addSignInPageDiff); await patch(AUTH_BACKEND_PLUGIN_FILE, replaceSignInResolverDiff); }; diff --git a/packages/cli/src/commands/admin/auth/index.ts b/packages/cli/src/commands/admin/auth/index.ts index 9af145dcf3..ce39748b09 100644 --- a/packages/cli/src/commands/admin/auth/index.ts +++ b/packages/cli/src/commands/admin/auth/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../../lib/tasks'; import { github } from './github'; @@ -42,4 +43,5 @@ export async function auth(): Promise { } Task.log(`Done setting up ${provider}!`); + Task.log(`You can now start you app with ${chalk.inverse('yarn dev')}`); } From eaece17af0b751acc8b0c25a39a048e2eacd9647 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 20 Feb 2023 12:59:18 +0100 Subject: [PATCH 14/34] Remove GHA approach for now Signed-off-by: Philipp Hugenroth --- .../cli/src/commands/admin/auth/github/app.ts | 44 ------------------- .../src/commands/admin/auth/github/index.ts | 22 +--------- packages/cli/src/commands/admin/auth/index.ts | 4 +- 3 files changed, 4 insertions(+), 66 deletions(-) delete mode 100644 packages/cli/src/commands/admin/auth/github/app.ts diff --git a/packages/cli/src/commands/admin/auth/github/app.ts b/packages/cli/src/commands/admin/auth/github/app.ts deleted file mode 100644 index d78fe2a3d2..0000000000 --- a/packages/cli/src/commands/admin/auth/github/app.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import inquirer from 'inquirer'; -import { adminCli } from '../../../create-github-app'; -import { updateConfigFile, updateEnvFile } from '../config'; -import { APP_CONFIG_FILE, ENV_CONFIG_FILE } from '../files'; - -// TODO(tudi2d): Wrapper for admin CLI around `create-github-app` - potentially to be removed -export const app = async (useEnvForSecrets: boolean) => { - // TODO(tudi2d): Make the GitHub Org optional - const input = await inquirer.prompt<{ org: string }>([ - { - type: 'input', - name: 'org', - message: 'Enter a GitHub Org [required]', - }, - ]); - - const { auth } = await adminCli(input.org); - - // TODO(tudi2d): Also change integrations - await updateConfigFile(APP_CONFIG_FILE, { auth }); - if (useEnvForSecrets) { - await updateEnvFile( - ENV_CONFIG_FILE, - auth.providers.github.development.clientId, - auth.providers.github.development.clientSecret, - ); - } -}; diff --git a/packages/cli/src/commands/admin/auth/github/index.ts b/packages/cli/src/commands/admin/auth/github/index.ts index d140b98dd3..fa22cacef8 100644 --- a/packages/cli/src/commands/admin/auth/github/index.ts +++ b/packages/cli/src/commands/admin/auth/github/index.ts @@ -15,7 +15,6 @@ */ import inquirer from 'inquirer'; -import { app } from './app'; import { oauth } from './oauth'; export const github = async () => { @@ -29,26 +28,9 @@ export const github = async () => { message: 'Would you like to store sensitive configuration details such as secrets as environment variables? (recommended)', }, - { - type: 'list', - name: 'type', - message: 'Do you want to use a Github App or Github OAuth?', - choices: ['GitHub OAuth', 'GitHub App'], - }, ]); - const { type, useEnvForSecrets } = answers; + const { useEnvForSecrets } = answers; - switch (type) { - case 'GitHub OAuth': { - await oauth(useEnvForSecrets); - break; - } - case 'GitHub App': { - await app(useEnvForSecrets); - break; - } - default: - throw new Error(`Unknown selection: ${type}.`); - } + await oauth(useEnvForSecrets); }; diff --git a/packages/cli/src/commands/admin/auth/index.ts b/packages/cli/src/commands/admin/auth/index.ts index ce39748b09..ce8e55556b 100644 --- a/packages/cli/src/commands/admin/auth/index.ts +++ b/packages/cli/src/commands/admin/auth/index.ts @@ -27,14 +27,14 @@ export async function auth(): Promise { type: 'list', name: 'provider', message: 'Please select a provider:', - choices: ['Github'], + choices: ['GitHub'], }, ]); const { provider } = answers; switch (provider) { - case 'Github': { + case 'GitHub': { await github(); break; } From 0a60451b6f3816e7a0f234416f3a8085dbf98031 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 20 Feb 2023 13:08:35 +0100 Subject: [PATCH 15/34] Revert create-github-app changes Signed-off-by: Philipp Hugenroth --- .../GithubCreateAppServer.ts | 7 +- .../src/commands/create-github-app/index.ts | 161 +++++------------- 2 files changed, 48 insertions(+), 120 deletions(-) diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts index cb2613c691..70b8c0ca53 100644 --- a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts +++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts @@ -33,7 +33,7 @@ const FORM_PAGE = ` `; -export type GithubAppConfig = { +type GithubAppConfig = { appId: number; slug?: string; name?: string; @@ -117,11 +117,8 @@ export class GithubCreateAppServer { ...(this.permissions.includes('members') && { members: 'read' }), ...(this.permissions.includes('read') && { contents: 'read' }), ...(this.permissions.includes('write') && { - administration: 'write', contents: 'write', - pull_requests: 'write', - issues: 'write', - workflows: 'write', + actions: 'write', }), }, name: 'Backstage-', diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts index 606e982bd4..6b3e732f18 100644 --- a/packages/cli/src/commands/create-github-app/index.ts +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -14,26 +14,61 @@ * limitations under the License. */ -import chalk from 'chalk'; import fs from 'fs-extra'; -import inquirer, { Question } from 'inquirer'; -import fetch from 'node-fetch'; -import openBrowser from 'react-dev-utils/openBrowser'; +import chalk from 'chalk'; import { stringify as stringifyYaml } from 'yaml'; -import { GithubAppConfig } from '@backstage/integration'; - +import inquirer, { Question, Answers } from 'inquirer'; import { paths } from '../../lib/paths'; import { GithubCreateAppServer } from './GithubCreateAppServer'; - +import fetch from 'node-fetch'; +import openBrowser from 'react-dev-utils/openBrowser'; // This is an experimental command that at this point does not support GitHub Enterprise // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest export default async (org: string) => { - // TODO(tudi2d): Why is the Org check not done here? - const answers = await permissionAnswers(); - await verifyGithubOrg(org); - const { fileName } = await createGHAYaml(org, answers.appType); + const answers: Answers = await inquirer.prompt({ + name: 'appType', + type: 'checkbox', + message: + 'Select permissions [required] (these can be changed later but then require approvals in all installations)', + choices: [ + { + name: 'Read access to content (required by Software Catalog to ingest data from repositories)', + value: 'read', + checked: true, + }, + { + name: 'Read access to members (required by Software Catalog to ingest GitHub teams)', + value: 'members', + checked: true, + }, + { + name: 'Read and Write to content and actions (required by Software Templates to create new repositories)', + value: 'write', + }, + ], + }); + if (answers.appType.length === 0) { + console.log(chalk.red('You must select at least one permission')); + process.exit(1); + } + + await verifyGithubOrg(org); + const { slug, name, ...config } = await GithubCreateAppServer.run({ + org, + permissions: answers.appType, + }); + + const fileName = `github-app-${slug}-credentials.yaml`; + const content = `# Name: ${name}\n${stringifyYaml(config)}`; + await fs.writeFile(paths.resolveTargetRoot(fileName), content); + console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); + console.log( + chalk.yellow( + 'This file contains sensitive credentials, it should not be committed to version control and handled with care!', + ), + ); console.log( "Here's an example on how to update the integrations section in app-config.yaml", ); @@ -47,44 +82,6 @@ integrations: ); }; -/** - * - * @param org string: GitHub Organisation - * @returns Promise: Partial App Config for Admin CLI - */ -export async function adminCli(org: string) { - await verifyGithubOrg(org); - const answers = await permissionAnswers({ - read: true, - members: true, - write: true, - }); - const { fileName, config } = await createGHAYaml(org, answers.appType); - const auth = { - providers: { - github: { - development: { - clientId: config.clientId, - clientSecret: config.clientSecret, - }, - }, - }, - }; - const integration = { - github: { - host: 'github.com', - apps: { - $include: `${fileName}`, - }, - }, - }; - - return { - auth, - integration, - }; -} - async function verifyGithubOrg(org: string): Promise { let response; @@ -123,7 +120,6 @@ async function verifyGithubOrg(org: string): Promise { openBrowser('https://github.com/account/organizations/new'); - // TODO(tudi2d): Could we maybe re-run the command rather than exit? console.log( chalk.yellow( 'Please re-run this command when you have created your new organization', @@ -133,68 +129,3 @@ async function verifyGithubOrg(org: string): Promise { process.exit(0); } } - -async function permissionAnswers( - checked: { read: boolean; members: boolean; write: boolean } = { - read: true, - members: true, - write: false, - }, -) { - const answers = await inquirer.prompt<{ appType: string[] }>({ - name: 'appType', - type: 'checkbox', - message: - 'Select permissions [required] (these can be changed later but then require approvals in all installations)', - choices: [ - { - name: 'Read access to content (required by Software Catalog to ingest data from repositories)', - value: 'read', - checked: checked.read, - }, - { - name: 'Read access to members (required by Software Catalog to ingest GitHub teams)', - value: 'members', - checked: checked.members, - }, - { - name: 'Read and Write to content and actions (required by Software Templates to create new repositories)', - value: 'write', - checked: checked.write, - }, - ], - }); - - if (answers.appType.length === 0) { - console.log(chalk.red('You must select at least one permission')); - // TODO(tudi2d): Maybe re-do input rather than exit? - process.exit(1); - } - - return answers; -} - -async function createGHAYaml( - org: string, - permissions: string[], -): Promise<{ fileName: string; config: GithubAppConfig }> { - const { slug, name, ...config } = await GithubCreateAppServer.run({ - org, - permissions, - }); - - const fileName = `github-app-${slug}-credentials.yaml`; - const content = `# Name: ${name}\n${stringifyYaml(config)}`; - await fs.writeFile(paths.resolveTargetRoot(fileName), content); - console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); - console.log( - chalk.yellow( - 'This file contains sensitive credentials, it should not be committed to version control and handled with care!', - ), - ); - - return { - fileName, - config, - }; -} From ed89e409653ab678f1191c9d67ee4faf7d3de2f7 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Mon, 20 Feb 2023 14:44:48 +0100 Subject: [PATCH 16/34] cli: Use .patch files instead Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/auth/files.ts | 20 ++++- .../src/commands/admin/auth/github/diffs.ts | 76 ------------------- .../src/commands/admin/auth/github/oauth.ts | 57 ++++++++++---- packages/cli/src/commands/admin/auth/index.ts | 4 +- packages/cli/src/commands/admin/auth/patch.ts | 5 +- .../auth/patches/01-github.App.tsx.patch | 24 ++++++ .../auth/patches/02-github.auth.ts.patch | 12 +++ 7 files changed, 100 insertions(+), 98 deletions(-) delete mode 100644 packages/cli/src/commands/admin/auth/github/diffs.ts create mode 100644 packages/cli/src/commands/admin/auth/patches/01-github.App.tsx.patch create mode 100644 packages/cli/src/commands/admin/auth/patches/02-github.auth.ts.patch diff --git a/packages/cli/src/commands/admin/auth/files.ts b/packages/cli/src/commands/admin/auth/files.ts index ad9b53a894..166538335f 100644 --- a/packages/cli/src/commands/admin/auth/files.ts +++ b/packages/cli/src/commands/admin/auth/files.ts @@ -18,19 +18,33 @@ import { findPaths } from '@backstage/cli-common'; import * as path from 'path'; /* eslint-disable-next-line no-restricted-syntax */ -const { targetRoot, resolveTargetRoot } = findPaths(__dirname); +const { targetRoot, ownDir, resolveTargetRoot } = findPaths(__dirname); export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); export const ENV_CONFIG_FILE = path.join(targetRoot, '.env.local'); export const USER_ENTITY_FILE = path.join(targetRoot, 'user-info.yaml'); -export const APP_TSX_FILE = path.join( +const APP_TSX_FILE = path.join( resolveTargetRoot('packages/app'), 'src', 'App.tsx', ); -export const AUTH_BACKEND_PLUGIN_FILE = path.join( +const AUTH_BACKEND_PLUGIN_FILE = path.join( resolveTargetRoot('packages/backend'), 'src', 'plugins', 'auth.ts', ); + +export const PATCH_FOLDER = path.join( + ownDir, + 'src', + 'commands', + 'admin', + 'auth', + 'patches', +); + +export const patchMap: Record = { + 'App.tsx': APP_TSX_FILE, + 'auth.ts': AUTH_BACKEND_PLUGIN_FILE, +}; diff --git a/packages/cli/src/commands/admin/auth/github/diffs.ts b/packages/cli/src/commands/admin/auth/github/diffs.ts deleted file mode 100644 index 43b6013d4e..0000000000 --- a/packages/cli/src/commands/admin/auth/github/diffs.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const addSignInPageDiff = - '@@ -32,11 +32,27 @@\n' + - " import { AppRouter, FlatRoutes } from '@backstage/core-app-api';\n" + - " import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';\n" + - " import { RequirePermission } from '@backstage/plugin-permission-react';\n" + - " import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';\n" + - "+import { githubAuthApiRef } from '@backstage/core-plugin-api';\n" + - "+import { SignInPage } from '@backstage/core-components';\n" + - ' \n' + - ' const app = createApp({\n' + - ' apis,\n' + - '+ components: {\n' + - '+ SignInPage: props => (\n' + - '+ \n' + - '+ ),\n' + - '+ },\n' + - ' bindRoutes({ bind }) {\n' + - ' bind(catalogPlugin.externalRoutes, {\n' + - ' createComponent: scaffolderPlugin.routes.root,\n' + - ' viewTechDoc: techdocsPlugin.routes.docRoot,\n'; - -export const replaceSignInResolverDiff = - '@@ -36,18 +36,18 @@\n' + - ' //\n' + - ' // https://backstage.io/docs/auth/identity-resolver\n' + - ' github: providers.github.create({\n' + - ' signIn: {\n' + - '- resolver(_, ctx) {\n' + - "- const userRef = 'user:default/guest'; // Must be a full entity reference\n" + - '- return ctx.issueToken({\n' + - '- claims: {\n' + - "- sub: userRef, // The user's own identity\n" + - '- ent: [userRef], // A list of identities that the user claims ownership through\n' + - '- },\n' + - '- });\n' + - '- },\n' + - '- // resolver: providers.github.resolvers.usernameMatchingUserEntityName(),\n' + - '+ // resolver(_, ctx) {\n' + - "+ // const userRef = 'user:default/guest'; // Must be a full entity reference\n" + - '+ // return ctx.issueToken({\n' + - '+ // claims: {\n' + - "+ // sub: userRef, // The user's own identity\n" + - '+ // ent: [userRef], // A list of identities that the user claims ownership through\n' + - '+ // },\n' + - '+ // });\n' + - '+ // },\n' + - '+ resolver: providers.github.resolvers.usernameMatchingUserEntityName(),\n' + - ' },\n' + - ' }),\n' + - ' },\n' + - ' });\n'; diff --git a/packages/cli/src/commands/admin/auth/github/oauth.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts index 64abf520cf..fe05b6431a 100644 --- a/packages/cli/src/commands/admin/auth/github/oauth.ts +++ b/packages/cli/src/commands/admin/auth/github/oauth.ts @@ -18,17 +18,18 @@ import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; import inquirer from 'inquirer'; import fetch from 'node-fetch'; +import * as fs from 'fs-extra'; +import * as path from 'path'; import { Task } from '../../../../lib/tasks'; import { addUserEntity, updateConfigFile, updateEnvFile } from '../config'; import { APP_CONFIG_FILE, - APP_TSX_FILE, - AUTH_BACKEND_PLUGIN_FILE, ENV_CONFIG_FILE, + patchMap, + PATCH_FOLDER, USER_ENTITY_FILE, } from '../files'; import { patch } from '../patch'; -import { addSignInPageDiff, replaceSignInResolverDiff } from './diffs'; const validateCredentials = async (clientId: string, clientSecret: string) => { try { @@ -50,7 +51,7 @@ const validateCredentials = async (clientId: string, clientSecret: string) => { error.response.status !== 200 && error.response.data.error !== 'bad_verification_code' ) { - throw new Error(`Validating Github Credentials failed.`); + throw new Error(`Validating GitHub Credentials failed.`); } } }; @@ -109,13 +110,14 @@ export const oauth = async (useEnvForSecrets: boolean) => { You can find the full documentation page here: ${chalk.blue( 'https://backstage.io/docs/auth/github/provider', - )}`); + )} + `); const answers = await inquirer.prompt([ { type: 'input', name: 'username', - message: 'What is your Github username?', + message: 'What is your GitHub username?', validate: async (input: string) => { const response = await fetch(`https://api.github.com/users/${input}`); if (!response.ok) { @@ -139,30 +141,53 @@ export const oauth = async (useEnvForSecrets: boolean) => { { type: 'confirm', name: 'hasEnterprise', - message: 'Are you using Github Enterprise?', + message: 'Are you using GitHub Enterprise?', }, { type: 'input', name: 'enterpriseInstanceUrl', - message: 'What is your URL for Github Enterprise?', + message: 'What is your URL for GitHub Enterprise?', when: ({ hasEnterprise }) => hasEnterprise, validate: (input: string) => Boolean(new URL(input)), }, ]); const { username, clientId, clientSecret } = answers; - - await validateCredentials(clientId, clientSecret); - const config = getConfig(answers, useEnvForSecrets); - await updateConfigFile(APP_CONFIG_FILE, config); + Task.log('Setting up GitHub Authentication for you...'); + + await Task.forItem( + 'Validating', + 'credentials', + async () => await validateCredentials(clientId, clientSecret), + ); + await Task.forItem( + 'Updating', + APP_CONFIG_FILE, + async () => await updateConfigFile(APP_CONFIG_FILE, config), + ); if (useEnvForSecrets) { - await updateEnvFile(ENV_CONFIG_FILE, clientId, clientSecret); + await Task.forItem( + 'Updating', + ENV_CONFIG_FILE, + async () => await updateEnvFile(ENV_CONFIG_FILE, clientId, clientSecret), + ); } + await Task.forItem( + 'Creating', + USER_ENTITY_FILE, + async () => await addUserEntity(USER_ENTITY_FILE, username), + ); - await addUserEntity(USER_ENTITY_FILE, username); + const patches = await fs.readdir(PATCH_FOLDER); + for (const patchFile of patches) { + const target = patchFile + .replace(/[0-9]+-github\./, '') + .replace('.patch', ''); - await patch(APP_TSX_FILE, addSignInPageDiff); - await patch(AUTH_BACKEND_PLUGIN_FILE, replaceSignInResolverDiff); + await Task.forItem('Pactching', target, async () => { + await patch(patchMap[target], path.join(PATCH_FOLDER, patchFile)); + }); + } }; diff --git a/packages/cli/src/commands/admin/auth/index.ts b/packages/cli/src/commands/admin/auth/index.ts index ce8e55556b..728a2ffc2c 100644 --- a/packages/cli/src/commands/admin/auth/index.ts +++ b/packages/cli/src/commands/admin/auth/index.ts @@ -43,5 +43,7 @@ export async function auth(): Promise { } Task.log(`Done setting up ${provider}!`); - Task.log(`You can now start you app with ${chalk.inverse('yarn dev')}`); + Task.log( + `You can now start you app with ${chalk.inverse(chalk.italic('yarn dev'))}`, + ); } diff --git a/packages/cli/src/commands/admin/auth/patch.ts b/packages/cli/src/commands/admin/auth/patch.ts index 2a4eccdb9e..cf466f9408 100644 --- a/packages/cli/src/commands/admin/auth/patch.ts +++ b/packages/cli/src/commands/admin/auth/patch.ts @@ -17,9 +17,10 @@ import * as fs from 'fs-extra'; import * as differ from 'diff'; -export const patch = async (file: string, diff: string) => { +export const patch = async (file: string, patchFile: string) => { + const patchContent = await fs.readFile(patchFile, 'utf8'); const oldContent = await fs.readFile(file, 'utf8'); - const newContent = differ.applyPatch(oldContent, diff); + const newContent = differ.applyPatch(oldContent, patchContent); return await fs.writeFile(file, newContent, 'utf8'); }; diff --git a/packages/cli/src/commands/admin/auth/patches/01-github.App.tsx.patch b/packages/cli/src/commands/admin/auth/patches/01-github.App.tsx.patch new file mode 100644 index 0000000000..a7e0c150a5 --- /dev/null +++ b/packages/cli/src/commands/admin/auth/patches/01-github.App.tsx.patch @@ -0,0 +1,24 @@ +@@ -30 +30,5 @@ import { Root } from './components/Root'; +-import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; ++import { ++ AlertDisplay, ++ OAuthRequestDialog, ++ SignInPage, ++} from '@backstage/core-components'; +@@ -35,0 +40 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/ ++import { githubAuthApiRef } from '@backstage/core-plugin-api'; +@@ -38,0 +44,14 @@ const app = createApp({ ++ components: { ++ SignInPage: props => ( ++ ++ ), ++ }, \ No newline at end of file diff --git a/packages/cli/src/commands/admin/auth/patches/02-github.auth.ts.patch b/packages/cli/src/commands/admin/auth/patches/02-github.auth.ts.patch new file mode 100644 index 0000000000..e6e65e7b74 --- /dev/null +++ b/packages/cli/src/commands/admin/auth/patches/02-github.auth.ts.patch @@ -0,0 +1,12 @@ +@@ -40,10 +40 @@ export default async function createPlugin( +- 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(), ++ resolver: providers.github.resolvers.usernameMatchingUserEntityName(), \ No newline at end of file From 20fc4735683b51bb81714757d018d31f6448b3d6 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 21 Feb 2023 13:22:57 +0100 Subject: [PATCH 17/34] Don't use environment variables Signed-off-by: Philipp Hugenroth --- .../cli/src/commands/admin/auth/config.ts | 16 ------------ packages/cli/src/commands/admin/auth/files.ts | 1 - .../src/commands/admin/auth/github/index.ts | 19 +------------- .../src/commands/admin/auth/github/oauth.ts | 26 ++++++------------- 4 files changed, 9 insertions(+), 53 deletions(-) diff --git a/packages/cli/src/commands/admin/auth/config.ts b/packages/cli/src/commands/admin/auth/config.ts index 659c356360..54c103cdf8 100644 --- a/packages/cli/src/commands/admin/auth/config.ts +++ b/packages/cli/src/commands/admin/auth/config.ts @@ -59,22 +59,6 @@ export const updateConfigFile = async ( ); }; -export const updateEnvFile = async ( - file: string, - clientId: string, - clientSecret: string, -) => { - const content = ` -AUTH_GITHUB_CLIENT_ID=${clientId} -AUTH_GITHUB_CLIENT_SECRET=${clientSecret}`; - - if (fs.existsSync(file)) { - return await fs.appendFile(file, content, 'utf8'); - } - - return await fs.writeFile(file, content, 'utf8'); -}; - export const addUserEntity = async (file: string, username: string) => { const content = { apiVersion: 'backstage.io/v1alpha1', diff --git a/packages/cli/src/commands/admin/auth/files.ts b/packages/cli/src/commands/admin/auth/files.ts index 166538335f..f84c77fe60 100644 --- a/packages/cli/src/commands/admin/auth/files.ts +++ b/packages/cli/src/commands/admin/auth/files.ts @@ -20,7 +20,6 @@ import * as path from 'path'; /* eslint-disable-next-line no-restricted-syntax */ const { targetRoot, ownDir, resolveTargetRoot } = findPaths(__dirname); export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); -export const ENV_CONFIG_FILE = path.join(targetRoot, '.env.local'); export const USER_ENTITY_FILE = path.join(targetRoot, 'user-info.yaml'); const APP_TSX_FILE = path.join( diff --git a/packages/cli/src/commands/admin/auth/github/index.ts b/packages/cli/src/commands/admin/auth/github/index.ts index fa22cacef8..cc2653bc05 100644 --- a/packages/cli/src/commands/admin/auth/github/index.ts +++ b/packages/cli/src/commands/admin/auth/github/index.ts @@ -14,23 +14,6 @@ * limitations under the License. */ -import inquirer from 'inquirer'; import { oauth } from './oauth'; -export const github = async () => { - const answers = await inquirer.prompt<{ - useEnvForSecrets: boolean; - type: string; - }>([ - { - type: 'confirm', - name: 'useEnvForSecrets', - message: - 'Would you like to store sensitive configuration details such as secrets as environment variables? (recommended)', - }, - ]); - - const { useEnvForSecrets } = answers; - - await oauth(useEnvForSecrets); -}; +export const github = async () => await oauth(); diff --git a/packages/cli/src/commands/admin/auth/github/oauth.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts index fe05b6431a..00bfbef23d 100644 --- a/packages/cli/src/commands/admin/auth/github/oauth.ts +++ b/packages/cli/src/commands/admin/auth/github/oauth.ts @@ -16,15 +16,14 @@ import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; +import * as fs from 'fs-extra'; import inquirer from 'inquirer'; import fetch from 'node-fetch'; -import * as fs from 'fs-extra'; import * as path from 'path'; import { Task } from '../../../../lib/tasks'; -import { addUserEntity, updateConfigFile, updateEnvFile } from '../config'; +import { addUserEntity, updateConfigFile } from '../config'; import { APP_CONFIG_FILE, - ENV_CONFIG_FILE, patchMap, PATCH_FOLDER, USER_ENTITY_FILE, @@ -56,7 +55,7 @@ const validateCredentials = async (clientId: string, clientSecret: string) => { } }; -const getConfig = (answers: Answers, useEnvForSecrets: boolean) => { +const getConfig = (answers: Answers) => { const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = answers; @@ -65,12 +64,10 @@ const getConfig = (answers: Answers, useEnvForSecrets: boolean) => { providers: { github: { development: { - clientId: useEnvForSecrets ? '${AUTH_GITHUB_CLIENT_ID}' : clientId, - clientSecret: useEnvForSecrets - ? '${AUTH_GITHUB_CLIENT_SECRET}' - : clientSecret, + clientId, + clientSecret, ...(hasEnterprise && { - enterpriseInstanceUrl: enterpriseInstanceUrl, + enterpriseInstanceUrl, }), }, }, @@ -96,7 +93,7 @@ type Answers = { enterpriseInstanceUrl?: string; }; -export const oauth = async (useEnvForSecrets: boolean) => { +export const oauth = async () => { Task.log(` To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( 'https://github.com/settings/developers', @@ -153,7 +150,7 @@ export const oauth = async (useEnvForSecrets: boolean) => { ]); const { username, clientId, clientSecret } = answers; - const config = getConfig(answers, useEnvForSecrets); + const config = getConfig(answers); Task.log('Setting up GitHub Authentication for you...'); @@ -167,13 +164,6 @@ export const oauth = async (useEnvForSecrets: boolean) => { APP_CONFIG_FILE, async () => await updateConfigFile(APP_CONFIG_FILE, config), ); - if (useEnvForSecrets) { - await Task.forItem( - 'Updating', - ENV_CONFIG_FILE, - async () => await updateEnvFile(ENV_CONFIG_FILE, clientId, clientSecret), - ); - } await Task.forItem( 'Creating', USER_ENTITY_FILE, From 977a5c8f96ab98d999eec729ab84bd72b37166a3 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 21 Feb 2023 15:17:52 +0100 Subject: [PATCH 18/34] cli: Simplify patching by reading target file from path Signed-off-by: Marcus Eide --- packages/cli/src/commands/admin/auth/files.ts | 20 +------------------ .../src/commands/admin/auth/github/oauth.ts | 16 +++------------ packages/cli/src/commands/admin/auth/patch.ts | 19 ++++++++++++++---- ...x.patch => 01-github.Add-SignInPage.patch} | 2 ++ ...ch => 02-github.Use-signIn-resolver.patch} | 2 ++ 5 files changed, 23 insertions(+), 36 deletions(-) rename packages/cli/src/commands/admin/auth/patches/{01-github.App.tsx.patch => 01-github.Add-SignInPage.patch} (92%) rename packages/cli/src/commands/admin/auth/patches/{02-github.auth.ts.patch => 02-github.Use-signIn-resolver.patch} (87%) diff --git a/packages/cli/src/commands/admin/auth/files.ts b/packages/cli/src/commands/admin/auth/files.ts index f84c77fe60..6ce58dc527 100644 --- a/packages/cli/src/commands/admin/auth/files.ts +++ b/packages/cli/src/commands/admin/auth/files.ts @@ -18,22 +18,9 @@ import { findPaths } from '@backstage/cli-common'; import * as path from 'path'; /* eslint-disable-next-line no-restricted-syntax */ -const { targetRoot, ownDir, resolveTargetRoot } = findPaths(__dirname); +const { targetRoot, ownDir } = findPaths(__dirname); export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); export const USER_ENTITY_FILE = path.join(targetRoot, 'user-info.yaml'); - -const APP_TSX_FILE = path.join( - resolveTargetRoot('packages/app'), - 'src', - 'App.tsx', -); -const AUTH_BACKEND_PLUGIN_FILE = path.join( - resolveTargetRoot('packages/backend'), - 'src', - 'plugins', - 'auth.ts', -); - export const PATCH_FOLDER = path.join( ownDir, 'src', @@ -42,8 +29,3 @@ export const PATCH_FOLDER = path.join( 'auth', 'patches', ); - -export const patchMap: Record = { - 'App.tsx': APP_TSX_FILE, - 'auth.ts': AUTH_BACKEND_PLUGIN_FILE, -}; diff --git a/packages/cli/src/commands/admin/auth/github/oauth.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts index 00bfbef23d..1de6b2a4c4 100644 --- a/packages/cli/src/commands/admin/auth/github/oauth.ts +++ b/packages/cli/src/commands/admin/auth/github/oauth.ts @@ -19,15 +19,9 @@ import chalk from 'chalk'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; import fetch from 'node-fetch'; -import * as path from 'path'; import { Task } from '../../../../lib/tasks'; import { addUserEntity, updateConfigFile } from '../config'; -import { - APP_CONFIG_FILE, - patchMap, - PATCH_FOLDER, - USER_ENTITY_FILE, -} from '../files'; +import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files'; import { patch } from '../patch'; const validateCredentials = async (clientId: string, clientSecret: string) => { @@ -172,12 +166,8 @@ export const oauth = async () => { const patches = await fs.readdir(PATCH_FOLDER); for (const patchFile of patches) { - const target = patchFile - .replace(/[0-9]+-github\./, '') - .replace('.patch', ''); - - await Task.forItem('Pactching', target, async () => { - await patch(patchMap[target], path.join(PATCH_FOLDER, patchFile)); + await Task.forItem('Patching', patchFile, async () => { + await patch(patchFile); }); } }; diff --git a/packages/cli/src/commands/admin/auth/patch.ts b/packages/cli/src/commands/admin/auth/patch.ts index cf466f9408..2333275064 100644 --- a/packages/cli/src/commands/admin/auth/patch.ts +++ b/packages/cli/src/commands/admin/auth/patch.ts @@ -15,12 +15,23 @@ */ import * as fs from 'fs-extra'; +import * as path from 'path'; import * as differ from 'diff'; +import { PATCH_FOLDER } from './files'; +import { findPaths } from '@backstage/cli-common'; -export const patch = async (file: string, patchFile: string) => { - const patchContent = await fs.readFile(patchFile, 'utf8'); - const oldContent = await fs.readFile(file, 'utf8'); +/* eslint-disable-next-line no-restricted-syntax */ +const { targetRoot } = findPaths(__dirname); + +export const patch = async (patchFile: string) => { + const patchContent = await fs.readFile( + path.join(PATCH_FOLDER, patchFile), + 'utf8', + ); + const targetName = patchContent.split('\n')[0].replace('--- a', ''); + const targetFile = path.join(targetRoot, targetName); + const oldContent = await fs.readFile(targetFile, 'utf8'); const newContent = differ.applyPatch(oldContent, patchContent); - return await fs.writeFile(file, newContent, 'utf8'); + return await fs.writeFile(targetFile, newContent, 'utf8'); }; diff --git a/packages/cli/src/commands/admin/auth/patches/01-github.App.tsx.patch b/packages/cli/src/commands/admin/auth/patches/01-github.Add-SignInPage.patch similarity index 92% rename from packages/cli/src/commands/admin/auth/patches/01-github.App.tsx.patch rename to packages/cli/src/commands/admin/auth/patches/01-github.Add-SignInPage.patch index a7e0c150a5..8a3401fea1 100644 --- a/packages/cli/src/commands/admin/auth/patches/01-github.App.tsx.patch +++ b/packages/cli/src/commands/admin/auth/patches/01-github.Add-SignInPage.patch @@ -1,3 +1,5 @@ +--- a/packages/app/src/App.tsx ++++ b/packages/app/src/App.tsx @@ -30 +30,5 @@ import { Root } from './components/Root'; -import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; +import { diff --git a/packages/cli/src/commands/admin/auth/patches/02-github.auth.ts.patch b/packages/cli/src/commands/admin/auth/patches/02-github.Use-signIn-resolver.patch similarity index 87% rename from packages/cli/src/commands/admin/auth/patches/02-github.auth.ts.patch rename to packages/cli/src/commands/admin/auth/patches/02-github.Use-signIn-resolver.patch index e6e65e7b74..2baaff7cf9 100644 --- a/packages/cli/src/commands/admin/auth/patches/02-github.auth.ts.patch +++ b/packages/cli/src/commands/admin/auth/patches/02-github.Use-signIn-resolver.patch @@ -1,3 +1,5 @@ +--- a/packages/backend/src/plugins/auth.ts ++++ b/packages/backend/src/plugins/auth.ts @@ -40,10 +40 @@ export default async function createPlugin( - resolver(_, ctx) { - const userRef = 'user:default/guest'; // Must be a full entity reference From 4e4a0507ca83cb4f309152e7265a14b48506d416 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 21 Feb 2023 15:22:27 +0100 Subject: [PATCH 19/34] cli: Consolidate oauth.ts into index.ts Signed-off-by: Marcus Eide --- .../src/commands/admin/auth/github/index.ts | 158 +++++++++++++++- .../src/commands/admin/auth/github/oauth.ts | 173 ------------------ 2 files changed, 156 insertions(+), 175 deletions(-) delete mode 100644 packages/cli/src/commands/admin/auth/github/oauth.ts diff --git a/packages/cli/src/commands/admin/auth/github/index.ts b/packages/cli/src/commands/admin/auth/github/index.ts index cc2653bc05..def97bf619 100644 --- a/packages/cli/src/commands/admin/auth/github/index.ts +++ b/packages/cli/src/commands/admin/auth/github/index.ts @@ -14,6 +14,160 @@ * limitations under the License. */ -import { oauth } from './oauth'; +import { OAuthApp } from '@octokit/oauth-app'; +import chalk from 'chalk'; +import * as fs from 'fs-extra'; +import inquirer from 'inquirer'; +import fetch from 'node-fetch'; +import { Task } from '../../../../lib/tasks'; +import { addUserEntity, updateConfigFile } from '../config'; +import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files'; +import { patch } from '../patch'; -export const github = async () => await oauth(); +const validateCredentials = async (clientId: string, clientSecret: string) => { + try { + const app = new OAuthApp({ + clientId, + clientSecret, + }); + await app.createToken({ + code: '%NOT-VALID-CODE%', + }); + } catch (error) { + // @octokit/request returns a error.response object when a request is rejected. + // We can check it to see what kind of error we received. + + // If error.response is successful we can double-check that the error itself was due to the bad code. + // If that's the case then we can assume that the client id and secret exists as we otherwise would + // have gotten a 400/404. + if ( + error.response.status !== 200 && + error.response.data.error !== 'bad_verification_code' + ) { + throw new Error(`Validating GitHub Credentials failed.`); + } + } +}; + +const getConfig = (answers: Answers) => { + const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = + answers; + + return { + auth: { + providers: { + github: { + development: { + clientId, + clientSecret, + ...(hasEnterprise && { + enterpriseInstanceUrl, + }), + }, + }, + }, + }, + catalog: { + locations: [ + { + type: 'file', + target: '../../user-info.yaml', + rules: [{ allow: ['User'] }], + }, + ], + }, + }; +}; + +type Answers = { + username: string; + clientSecret: string; + clientId: string; + hasEnterprise: boolean; + enterpriseInstanceUrl?: string; +}; + +export const github = async () => { + Task.log(` + To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( + 'https://github.com/settings/developers', + )} + The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend. + + Settings for local development: + ${chalk.cyan(` + Homepage URL: http://localhost:3000 + Authorization callback URL: http://localhost:7007/api/auth/github/handler/frame`)} + + You can find the full documentation page here: ${chalk.blue( + 'https://backstage.io/docs/auth/github/provider', + )} + `); + + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: 'What is your GitHub username?', + validate: async (input: string) => { + const response = await fetch(`https://api.github.com/users/${input}`); + if (!response.ok) { + return chalk.red('Unknown user. Please try again.'); + } + return true; + }, + }, + { + type: 'input', + name: 'clientId', + message: 'What is your Client Id?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'input', + name: 'clientSecret', + message: 'What is your Client Secret?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'confirm', + name: 'hasEnterprise', + message: 'Are you using GitHub Enterprise?', + }, + { + type: 'input', + name: 'enterpriseInstanceUrl', + message: 'What is your URL for GitHub Enterprise?', + when: ({ hasEnterprise }) => hasEnterprise, + validate: (input: string) => Boolean(new URL(input)), + }, + ]); + + const { username, clientId, clientSecret } = answers; + const config = getConfig(answers); + + Task.log('Setting up GitHub Authentication for you...'); + + await Task.forItem( + 'Validating', + 'credentials', + async () => await validateCredentials(clientId, clientSecret), + ); + await Task.forItem( + 'Updating', + APP_CONFIG_FILE, + async () => await updateConfigFile(APP_CONFIG_FILE, config), + ); + await Task.forItem( + 'Creating', + USER_ENTITY_FILE, + async () => await addUserEntity(USER_ENTITY_FILE, username), + ); + + const patches = await fs.readdir(PATCH_FOLDER); + for (const patchFile of patches) { + await Task.forItem('Patching', patchFile, async () => { + await patch(patchFile); + }); + } +}; diff --git a/packages/cli/src/commands/admin/auth/github/oauth.ts b/packages/cli/src/commands/admin/auth/github/oauth.ts deleted file mode 100644 index 1de6b2a4c4..0000000000 --- a/packages/cli/src/commands/admin/auth/github/oauth.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { OAuthApp } from '@octokit/oauth-app'; -import chalk from 'chalk'; -import * as fs from 'fs-extra'; -import inquirer from 'inquirer'; -import fetch from 'node-fetch'; -import { Task } from '../../../../lib/tasks'; -import { addUserEntity, updateConfigFile } from '../config'; -import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files'; -import { patch } from '../patch'; - -const validateCredentials = async (clientId: string, clientSecret: string) => { - try { - const app = new OAuthApp({ - clientId, - clientSecret, - }); - await app.createToken({ - code: '%NOT-VALID-CODE%', - }); - } catch (error) { - // @octokit/request returns a error.response object when a request is rejected. - // We can check it to see what kind of error we received. - - // If error.response is successful we can double-check that the error itself was due to the bad code. - // If that's the case then we can assume that the client id and secret exists as we otherwise would - // have gotten a 400/404. - if ( - error.response.status !== 200 && - error.response.data.error !== 'bad_verification_code' - ) { - throw new Error(`Validating GitHub Credentials failed.`); - } - } -}; - -const getConfig = (answers: Answers) => { - const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = - answers; - - return { - auth: { - providers: { - github: { - development: { - clientId, - clientSecret, - ...(hasEnterprise && { - enterpriseInstanceUrl, - }), - }, - }, - }, - }, - catalog: { - locations: [ - { - type: 'file', - target: '../../user-info.yaml', - rules: [{ allow: ['User'] }], - }, - ], - }, - }; -}; - -type Answers = { - username: string; - clientSecret: string; - clientId: string; - hasEnterprise: boolean; - enterpriseInstanceUrl?: string; -}; - -export const oauth = async () => { - Task.log(` - To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( - 'https://github.com/settings/developers', - )} - The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend. - - Settings for local development: - ${chalk.cyan(` - Homepage URL: http://localhost:3000 - Authorization callback URL: http://localhost:7007/api/auth/github/handler/frame`)} - - You can find the full documentation page here: ${chalk.blue( - 'https://backstage.io/docs/auth/github/provider', - )} - `); - - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'What is your GitHub username?', - validate: async (input: string) => { - const response = await fetch(`https://api.github.com/users/${input}`); - if (!response.ok) { - return chalk.red('Unknown user. Please try again.'); - } - return true; - }, - }, - { - type: 'input', - name: 'clientId', - message: 'What is your Client Id?', - validate: (input: string) => (input.length ? true : false), - }, - { - type: 'input', - name: 'clientSecret', - message: 'What is your Client Secret?', - validate: (input: string) => (input.length ? true : false), - }, - { - type: 'confirm', - name: 'hasEnterprise', - message: 'Are you using GitHub Enterprise?', - }, - { - type: 'input', - name: 'enterpriseInstanceUrl', - message: 'What is your URL for GitHub Enterprise?', - when: ({ hasEnterprise }) => hasEnterprise, - validate: (input: string) => Boolean(new URL(input)), - }, - ]); - - const { username, clientId, clientSecret } = answers; - const config = getConfig(answers); - - Task.log('Setting up GitHub Authentication for you...'); - - await Task.forItem( - 'Validating', - 'credentials', - async () => await validateCredentials(clientId, clientSecret), - ); - await Task.forItem( - 'Updating', - APP_CONFIG_FILE, - async () => await updateConfigFile(APP_CONFIG_FILE, config), - ); - await Task.forItem( - 'Creating', - USER_ENTITY_FILE, - async () => await addUserEntity(USER_ENTITY_FILE, username), - ); - - const patches = await fs.readdir(PATCH_FOLDER); - for (const patchFile of patches) { - await Task.forItem('Patching', patchFile, async () => { - await patch(patchFile); - }); - } -}; From e1771deebb82cf83b7efda65d9a3ce5456f4adf5 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 21 Feb 2023 15:27:53 +0100 Subject: [PATCH 20/34] cli: Rename command to onboard Signed-off-by: Marcus Eide --- packages/cli/src/commands/index.ts | 8 ++++---- packages/cli/src/commands/{admin => onboard}/README.md | 0 .../cli/src/commands/{admin => onboard}/auth/config.ts | 0 .../cli/src/commands/{admin => onboard}/auth/files.ts | 2 +- .../src/commands/{admin => onboard}/auth/github/index.ts | 0 .../cli/src/commands/{admin => onboard}/auth/index.ts | 0 .../cli/src/commands/{admin => onboard}/auth/patch.ts | 0 .../auth/patches/01-github.Add-SignInPage.patch | 0 .../auth/patches/02-github.Use-signIn-resolver.patch | 0 packages/cli/src/commands/{admin => onboard}/command.ts | 0 packages/cli/src/commands/{admin => onboard}/index.ts | 0 11 files changed, 5 insertions(+), 5 deletions(-) rename packages/cli/src/commands/{admin => onboard}/README.md (100%) rename packages/cli/src/commands/{admin => onboard}/auth/config.ts (100%) rename packages/cli/src/commands/{admin => onboard}/auth/files.ts (98%) rename packages/cli/src/commands/{admin => onboard}/auth/github/index.ts (100%) rename packages/cli/src/commands/{admin => onboard}/auth/index.ts (100%) rename packages/cli/src/commands/{admin => onboard}/auth/patch.ts (100%) rename packages/cli/src/commands/{admin => onboard}/auth/patches/01-github.Add-SignInPage.patch (100%) rename packages/cli/src/commands/{admin => onboard}/auth/patches/02-github.Use-signIn-resolver.patch (100%) rename packages/cli/src/commands/{admin => onboard}/command.ts (100%) rename packages/cli/src/commands/{admin => onboard}/index.ts (100%) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index df0689b4b5..ba11a53cfd 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -25,11 +25,11 @@ const configOption = [ Array(), ] as const; -export function registerAdminCommand(program: Command) { +export function registerOnboardCommand(program: Command) { program - .command('admin', { hidden: true }) + .command('onboard', { hidden: true }) .description('Get help setting up your Backstage App.') - .action(lazy(() => import('./admin').then(m => m.command))); + .action(lazy(() => import('./onboard').then(m => m.command))); } export function registerRepoCommand(program: Command) { @@ -367,7 +367,7 @@ export function registerCommands(program: Command) { registerRepoCommand(program); registerScriptCommand(program); registerMigrateCommand(program); - registerAdminCommand(program); + registerOnboardCommand(program); program .command('versions:bump') diff --git a/packages/cli/src/commands/admin/README.md b/packages/cli/src/commands/onboard/README.md similarity index 100% rename from packages/cli/src/commands/admin/README.md rename to packages/cli/src/commands/onboard/README.md diff --git a/packages/cli/src/commands/admin/auth/config.ts b/packages/cli/src/commands/onboard/auth/config.ts similarity index 100% rename from packages/cli/src/commands/admin/auth/config.ts rename to packages/cli/src/commands/onboard/auth/config.ts diff --git a/packages/cli/src/commands/admin/auth/files.ts b/packages/cli/src/commands/onboard/auth/files.ts similarity index 98% rename from packages/cli/src/commands/admin/auth/files.ts rename to packages/cli/src/commands/onboard/auth/files.ts index 6ce58dc527..af07ef254b 100644 --- a/packages/cli/src/commands/admin/auth/files.ts +++ b/packages/cli/src/commands/onboard/auth/files.ts @@ -25,7 +25,7 @@ export const PATCH_FOLDER = path.join( ownDir, 'src', 'commands', - 'admin', + 'onboard', 'auth', 'patches', ); diff --git a/packages/cli/src/commands/admin/auth/github/index.ts b/packages/cli/src/commands/onboard/auth/github/index.ts similarity index 100% rename from packages/cli/src/commands/admin/auth/github/index.ts rename to packages/cli/src/commands/onboard/auth/github/index.ts diff --git a/packages/cli/src/commands/admin/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts similarity index 100% rename from packages/cli/src/commands/admin/auth/index.ts rename to packages/cli/src/commands/onboard/auth/index.ts diff --git a/packages/cli/src/commands/admin/auth/patch.ts b/packages/cli/src/commands/onboard/auth/patch.ts similarity index 100% rename from packages/cli/src/commands/admin/auth/patch.ts rename to packages/cli/src/commands/onboard/auth/patch.ts diff --git a/packages/cli/src/commands/admin/auth/patches/01-github.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch similarity index 100% rename from packages/cli/src/commands/admin/auth/patches/01-github.Add-SignInPage.patch rename to packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch diff --git a/packages/cli/src/commands/admin/auth/patches/02-github.Use-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/02-github.Use-signIn-resolver.patch similarity index 100% rename from packages/cli/src/commands/admin/auth/patches/02-github.Use-signIn-resolver.patch rename to packages/cli/src/commands/onboard/auth/patches/02-github.Use-signIn-resolver.patch diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/onboard/command.ts similarity index 100% rename from packages/cli/src/commands/admin/command.ts rename to packages/cli/src/commands/onboard/command.ts diff --git a/packages/cli/src/commands/admin/index.ts b/packages/cli/src/commands/onboard/index.ts similarity index 100% rename from packages/cli/src/commands/admin/index.ts rename to packages/cli/src/commands/onboard/index.ts From c07c3b7364bcfa3e1f861997e61c9717fa8a2c27 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 22 Feb 2023 09:49:14 +0100 Subject: [PATCH 21/34] cli: Add changeset Signed-off-by: Marcus Eide --- .changeset/sour-dragons-cheat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sour-dragons-cheat.md diff --git a/.changeset/sour-dragons-cheat.md b/.changeset/sour-dragons-cheat.md new file mode 100644 index 0000000000..1b577d85f7 --- /dev/null +++ b/.changeset/sour-dragons-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add `onboard` command. While still in development, this command aims to guide users in setting up their Backstage App. From 284810a0a1c4e0d76cf98b7390bf2daaea6ff1c1 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 22 Feb 2023 10:45:17 +0100 Subject: [PATCH 22/34] cli: Add better error message if a patch failed Signed-off-by: Marcus Eide --- packages/cli/src/commands/onboard/auth/patch.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/cli/src/commands/onboard/auth/patch.ts b/packages/cli/src/commands/onboard/auth/patch.ts index 2333275064..2ebca2aab2 100644 --- a/packages/cli/src/commands/onboard/auth/patch.ts +++ b/packages/cli/src/commands/onboard/auth/patch.ts @@ -32,6 +32,12 @@ export const patch = async (patchFile: string) => { const targetFile = path.join(targetRoot, targetName); const oldContent = await fs.readFile(targetFile, 'utf8'); const newContent = differ.applyPatch(oldContent, patchContent); + if (!newContent) { + throw new Error( + `Patch ${patchFile} was not applied correctly. + Did you change ${targetName} manually before running this command?`, + ); + } return await fs.writeFile(targetFile, newContent, 'utf8'); }; From ff8bffb063ec36d72fc3bf959c3fc1a881238fa4 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 22 Feb 2023 10:46:25 +0100 Subject: [PATCH 23/34] cli: Remove unused answer variable Signed-off-by: Marcus Eide --- packages/cli/src/commands/onboard/command.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/commands/onboard/command.ts b/packages/cli/src/commands/onboard/command.ts index 4c49631a80..5bad1211ab 100644 --- a/packages/cli/src/commands/onboard/command.ts +++ b/packages/cli/src/commands/onboard/command.ts @@ -21,7 +21,6 @@ import { auth } from './auth'; export async function command(): Promise { const answers = await inquirer.prompt<{ shouldSetupAuth: boolean; - provider?: string; }>([ { type: 'confirm', From 711f8c427290547f680fef3b26306ce895fc85b9 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 22 Feb 2023 10:57:26 +0100 Subject: [PATCH 24/34] cli: Fix typo Signed-off-by: Marcus Eide --- packages/cli/src/commands/onboard/auth/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/onboard/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts index 728a2ffc2c..279a8cea7d 100644 --- a/packages/cli/src/commands/onboard/auth/index.ts +++ b/packages/cli/src/commands/onboard/auth/index.ts @@ -42,8 +42,11 @@ export async function auth(): Promise { throw new Error(`Provider ${provider} not implemented yet.`); } - Task.log(`Done setting up ${provider}!`); + Task.log(`Done setting up ${provider} Authentication!`); + Task.log(); Task.log( - `You can now start you app with ${chalk.inverse(chalk.italic('yarn dev'))}`, + `You can now start your app with ${chalk.inverse( + chalk.italic('yarn dev'), + )}`, ); } From c24703bd5aeda4feb606fe753ab37e621d7c914d Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 24 Feb 2023 15:28:02 +0100 Subject: [PATCH 25/34] cli: Add GitLab option to setting up auth Signed-off-by: Marcus Eide --- .../cli/src/commands/onboard/auth/config.ts | 43 +++--- .../src/commands/onboard/auth/github/index.ts | 16 +-- .../src/commands/onboard/auth/gitlab/index.ts | 126 ++++++++++++++++++ .../cli/src/commands/onboard/auth/index.ts | 7 +- .../patches/01-github.Add-SignInPage.patch | 1 - .../patches/03-gitlab.Add-SignInPage.patch | 25 ++++ .../04-gitlab.Use-signIn-resolver.patch | 27 ++++ 7 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 packages/cli/src/commands/onboard/auth/gitlab/index.ts create mode 100644 packages/cli/src/commands/onboard/auth/patches/03-gitlab.Add-SignInPage.patch create mode 100644 packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch diff --git a/packages/cli/src/commands/onboard/auth/config.ts b/packages/cli/src/commands/onboard/auth/config.ts index 54c103cdf8..01afc5f96c 100644 --- a/packages/cli/src/commands/onboard/auth/config.ts +++ b/packages/cli/src/commands/onboard/auth/config.ts @@ -14,28 +14,35 @@ * limitations under the License. */ +import { UserEntity } from '@backstage/catalog-model'; import * as fs from 'fs-extra'; import yaml from 'yaml'; -type GithubAuthConfig = { +type AuthConfig = { auth: { providers: { - github: { + [key: string]: { development: { clientId: string; clientSecret: string; enterpriseInstanceUrl?: string; + audience?: string; }; }; }; }; - catalog?: { - locations: Array<{ - type: string; - target: string; - rules: Array<{ allow: Array }>; - }>; - }; +}; + +const catalogUserLocation = { + catalog: { + locations: [ + { + type: 'file', + target: '../../user-info.yaml', + rules: [{ allow: ['User'] }], + }, + ], + }, }; const readYaml = async (file: string) => { @@ -44,11 +51,11 @@ const readYaml = async (file: string) => { export const updateConfigFile = async ( file: string, - config: GithubAuthConfig, + authConfig: AuthConfig, ) => { const content = fs.existsSync(file) - ? { ...(await readYaml(file)), ...config } - : config; + ? { ...(await readYaml(file)), ...authConfig, ...catalogUserLocation } + : { ...authConfig, ...catalogUserLocation }; return await fs.writeFile( file, @@ -59,15 +66,17 @@ export const updateConfigFile = async ( ); }; -export const addUserEntity = async (file: string, username: string) => { - const content = { +export const addUserEntity = async ( + file: string, + username: string, + annotations?: Record, +) => { + const content: UserEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { name: username, - annotations: { - 'github.com/user-login': username, - }, + annotations: { ...annotations }, }, spec: { memberOf: [], diff --git a/packages/cli/src/commands/onboard/auth/github/index.ts b/packages/cli/src/commands/onboard/auth/github/index.ts index def97bf619..c1789fa64d 100644 --- a/packages/cli/src/commands/onboard/auth/github/index.ts +++ b/packages/cli/src/commands/onboard/auth/github/index.ts @@ -67,15 +67,6 @@ const getConfig = (answers: Answers) => { }, }, }, - catalog: { - locations: [ - { - type: 'file', - target: '../../user-info.yaml', - rules: [{ allow: ['User'] }], - }, - ], - }, }; }; @@ -161,11 +152,14 @@ export const github = async () => { await Task.forItem( 'Creating', USER_ENTITY_FILE, - async () => await addUserEntity(USER_ENTITY_FILE, username), + async () => + await addUserEntity(USER_ENTITY_FILE, username, { + 'github.com/user-login': username, + }), ); const patches = await fs.readdir(PATCH_FOLDER); - for (const patchFile of patches) { + for (const patchFile of patches.filter(p => p.includes('github'))) { await Task.forItem('Patching', patchFile, async () => { await patch(patchFile); }); diff --git a/packages/cli/src/commands/onboard/auth/gitlab/index.ts b/packages/cli/src/commands/onboard/auth/gitlab/index.ts new file mode 100644 index 0000000000..4fa77f6b02 --- /dev/null +++ b/packages/cli/src/commands/onboard/auth/gitlab/index.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import * as fs from 'fs-extra'; +import inquirer from 'inquirer'; +import { Task } from '../../../../lib/tasks'; +import { addUserEntity, updateConfigFile } from '../config'; +import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files'; +import { patch } from '../patch'; + +const getConfig = (answers: Answers) => { + const { clientId, clientSecret, hasAudience, audience } = answers; + + return { + auth: { + providers: { + gitlab: { + development: { + clientId, + clientSecret, + ...(hasAudience && { + audience, + }), + }, + }, + }, + }, + }; +}; + +type Answers = { + username: string; + clientSecret: string; + clientId: string; + hasAudience: boolean; + audience?: string; +}; + +export const gitlab = async () => { + Task.log(` + To add GitLub authentication, you must create an Application from the GitLab Settings: ${chalk.blue( + 'https://gitlab.com/-/profile/applications', + )} + The Redirect URI should point to your Backstage backend auth handler. + + Settings for local development: + ${chalk.cyan(` + Name: Backstage (or your custom app name) + Redirect URI: http://localhost:7007/api/auth/gitlab/handler/frame + Scopes: read_api and read_user`)} + + You can find the full documentation page here: ${chalk.blue( + 'https://backstage.io/docs/auth/gitlab/provider', + )} + `); + + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: 'What is your GitLab username?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'input', + name: 'clientId', + message: 'What is your Application Id?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'input', + name: 'clientSecret', + message: 'What is your Application Secret?', + validate: (input: string) => (input.length ? true : false), + }, + { + type: 'confirm', + name: 'hasAudience', + message: 'Do you have a self-hosted instance of GitLab?', + }, + { + type: 'input', + name: 'audience', + message: 'What is the URL for your GitLab instance?', + when: ({ hasAudience }) => hasAudience, + validate: (input: string) => Boolean(new URL(input)), + }, + ]); + + const { username } = answers; + const config = getConfig(answers); + + Task.log('Setting up GitLab Authentication for you...'); + + await Task.forItem( + 'Updating', + APP_CONFIG_FILE, + async () => await updateConfigFile(APP_CONFIG_FILE, config), + ); + await Task.forItem( + 'Creating', + USER_ENTITY_FILE, + async () => await addUserEntity(USER_ENTITY_FILE, username), + ); + + const patches = await fs.readdir(PATCH_FOLDER); + for (const patchFile of patches.filter(p => p.includes('gitlab'))) { + await Task.forItem('Patching', patchFile, async () => { + await patch(patchFile); + }); + } +}; diff --git a/packages/cli/src/commands/onboard/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts index 279a8cea7d..df9471e1db 100644 --- a/packages/cli/src/commands/onboard/auth/index.ts +++ b/packages/cli/src/commands/onboard/auth/index.ts @@ -18,6 +18,7 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../../lib/tasks'; import { github } from './github'; +import { gitlab } from './gitlab'; export async function auth(): Promise { const answers = await inquirer.prompt<{ @@ -27,7 +28,7 @@ export async function auth(): Promise { type: 'list', name: 'provider', message: 'Please select a provider:', - choices: ['GitHub'], + choices: ['GitHub', 'GitLab'], }, ]); @@ -38,6 +39,10 @@ export async function auth(): Promise { await github(); break; } + case 'GitLab': { + await gitlab(); + break; + } default: throw new Error(`Provider ${provider} not implemented yet.`); } diff --git a/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch index 8a3401fea1..db9f5228e8 100644 --- a/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch +++ b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch @@ -14,7 +14,6 @@ + SignInPage: props => ( + ( ++ ++ ), ++ }, \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch new file mode 100644 index 0000000000..04b432c179 --- /dev/null +++ b/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch @@ -0,0 +1,27 @@ +--- a/packages/backend/src/plugins/auth.ts ++++ b/packages/backend/src/plugins/auth.ts +@@ -38 +38 @@ export default async function createPlugin( +- github: providers.github.create({ ++ gitlab: providers.gitlab.create({ +@@ -40,8 +40,11 @@ export default async function createPlugin( +- 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 +- }, +- }); ++ async resolver(info, ctx) { ++ const { fullProfile } = info.result; ++ ++ const userId = fullProfile.username; ++ if (!userId) { ++ throw new Error( ++ `GitLab user profile does not contain a username`, ++ ); ++ } ++ ++ return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); +@@ -49 +51,0 @@ export default async function createPlugin( +- // resolver: providers.github.resolvers.usernameMatchingUserEntityName(), \ No newline at end of file From 55f802caa65afe8505f0d83b70de525f9aecb045 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 27 Feb 2023 19:00:45 +0100 Subject: [PATCH 26/34] Add GitHub integration to Onboard CLI Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/onboard/command.ts | 64 +++++++++++----- .../onboard/integrations/github/index.ts | 73 +++++++++++++++++++ .../commands/onboard/integrations/index.ts | 49 +++++++++++++ 3 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 packages/cli/src/commands/onboard/integrations/github/index.ts create mode 100644 packages/cli/src/commands/onboard/integrations/index.ts diff --git a/packages/cli/src/commands/onboard/command.ts b/packages/cli/src/commands/onboard/command.ts index 5bad1211ab..1cfaabed12 100644 --- a/packages/cli/src/commands/onboard/command.ts +++ b/packages/cli/src/commands/onboard/command.ts @@ -17,26 +17,54 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { auth } from './auth'; +import { integrations } from './integrations'; export async function command(): Promise { - const answers = await inquirer.prompt<{ - shouldSetupAuth: boolean; - }>([ - { - type: 'confirm', - name: 'shouldSetupAuth', - message: 'Do you want to set up Authentication for this project?', - }, - ]); + try { + // TODO(tudi2d): Is there a way to make this cleaner? + const answers = await inquirer.prompt<{ + shouldSetupAuth: boolean; + }>([ + { + type: 'confirm', + name: 'shouldSetupAuth', + message: 'Do you want to set up Authentication for this project?', + default: true, + }, + ]); - if (!answers.shouldSetupAuth) { - console.log( - chalk.yellow( - 'If you change your mind, feel free to re-run this command.', - ), - ); - process.exit(1); + if (!answers.shouldSetupAuth) { + console.log( + chalk.yellow( + 'If you change your mind, feel free to re-run this command.', + ), + ); + } else { + await auth(); + } + + // TODO(tudi2d): Select "Core Features" here such that we can determin if the integrations need to be setup for either Catalog, Scaffollder or both + await inquirer + .prompt<{ + setupScaffolder: boolean; + }>([ + { + type: 'confirm', + name: 'setupScaffolder', + message: 'Do you want to setup Sofware Templates?', + default: true, + }, + ]) + .then(async ({ setupScaffolder }) => + setupScaffolder + ? await integrations() + : console.log( + chalk.yellow( + 'If you change your mind, feel free to re-run this command.', + ), + ), + ); + } catch (err) { + process.exit(-1); } - - await auth(); } diff --git a/packages/cli/src/commands/onboard/integrations/github/index.ts b/packages/cli/src/commands/onboard/integrations/github/index.ts new file mode 100644 index 0000000000..73416699e4 --- /dev/null +++ b/packages/cli/src/commands/onboard/integrations/github/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { Task } from '../../../../lib/tasks'; + +export const github = async () => { + const host = 'https://github.com'; + const answers = await inquirer.prompt<{ hasEnterprise: boolean }>([ + { + type: 'confirm', + name: 'hasEnterprise', + message: 'Are you using GitHub Enterprise?', + }, + ]); + + if (answers.hasEnterprise) { + await inquirer.prompt<{ hasEnterprise: boolean }>([ + { + type: 'confirm', + name: 'hasEnterprise', + message: 'Are you using GitHub Enterprise?', + }, + { + type: 'input', + name: 'enterpriseInstanceUrl', + message: 'What is your GitHub Enterprise URL (e.g. ghe.example.net)?', + when: ({ hasEnterprise }) => hasEnterprise, + validate: (input: string) => Boolean(new URL(input)), + }, + ]); + } + + Task.log(` + To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk.blue( + `${host}/settings/tokens/new', + )} + + Select the following scopes: + + Reading software components:${chalk.cyan(` + - "repo"`)} + + Reading organization data:${chalk.cyan(` + - "read:org" + - "read:user" + - "user:email"`)} + + Publishing software templates:${chalk.cyan(` + - "repo" + - "workflow" (if templates include GitHub workflows) + `)} + + You can find the full documentation page here: ${chalk.blue( + 'https://backstage.io/docs/integrations/github/locations', + )} + `, + )}`); +}; diff --git a/packages/cli/src/commands/onboard/integrations/index.ts b/packages/cli/src/commands/onboard/integrations/index.ts new file mode 100644 index 0000000000..2b55ab2bba --- /dev/null +++ b/packages/cli/src/commands/onboard/integrations/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import inquirer from 'inquirer'; +import { github } from './github'; + +enum Integration { + GITHUB = 'GitHub', +} + +const Integrations: Integration[] = [Integration.GITHUB]; + +export async function integrations(): Promise { + await inquirer + .prompt<{ + integration?: Integration; + }>([ + { + // TODO(tudi2d): Should be multiple choice + type: 'list', + name: 'integration', + message: 'Please select an integration:', + choices: Integrations, + }, + ]) + .then(async ({ integration }) => { + if (integration) { + switch (integration) { + case Integration.GITHUB: + await github(); + break; + default: + } + } + }); +} From 87dc2beb2179cedd9c25e2f76e5ac450ffe6a52d Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 1 Mar 2023 10:10:12 +0100 Subject: [PATCH 27/34] Setup basic GitHub & GHE integration using CLI Signed-off-by: Philipp Hugenroth --- .../src/commands/onboard/auth/github/index.ts | 38 ++++-- .../src/commands/onboard/auth/gitlab/index.ts | 4 +- .../cli/src/commands/onboard/auth/patch.ts | 2 +- .../src/commands/onboard/{auth => }/config.ts | 36 +----- .../src/commands/onboard/{auth => }/files.ts | 0 .../onboard/integrations/github/index.ts | 113 ++++++++++++++---- .../commands/onboard/integrations/index.ts | 2 +- 7 files changed, 126 insertions(+), 69 deletions(-) rename packages/cli/src/commands/onboard/{auth => }/config.ts (68%) rename packages/cli/src/commands/onboard/{auth => }/files.ts (100%) diff --git a/packages/cli/src/commands/onboard/auth/github/index.ts b/packages/cli/src/commands/onboard/auth/github/index.ts index c1789fa64d..ce53d23ad3 100644 --- a/packages/cli/src/commands/onboard/auth/github/index.ts +++ b/packages/cli/src/commands/onboard/auth/github/index.ts @@ -20,10 +20,30 @@ import * as fs from 'fs-extra'; import inquirer from 'inquirer'; import fetch from 'node-fetch'; import { Task } from '../../../../lib/tasks'; -import { addUserEntity, updateConfigFile } from '../config'; -import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files'; +import { addUserEntity, updateConfigFile } from '../../config'; +import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../../files'; import { patch } from '../patch'; +type Answers = { + username: string; + clientSecret: string; + clientId: string; + hasEnterprise: boolean; + enterpriseInstanceUrl?: string; +}; + +const catalogUserLocation = { + catalog: { + locations: [ + { + type: 'file', + target: '../../user-info.yaml', + rules: [{ allow: ['User'] }], + }, + ], + }, +}; + const validateCredentials = async (clientId: string, clientSecret: string) => { try { const app = new OAuthApp({ @@ -70,14 +90,6 @@ const getConfig = (answers: Answers) => { }; }; -type Answers = { - username: string; - clientSecret: string; - clientId: string; - hasEnterprise: boolean; - enterpriseInstanceUrl?: string; -}; - export const github = async () => { Task.log(` To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( @@ -147,7 +159,11 @@ export const github = async () => { await Task.forItem( 'Updating', APP_CONFIG_FILE, - async () => await updateConfigFile(APP_CONFIG_FILE, config), + async () => + await updateConfigFile(APP_CONFIG_FILE, { + ...config, + ...catalogUserLocation, + }), ); await Task.forItem( 'Creating', diff --git a/packages/cli/src/commands/onboard/auth/gitlab/index.ts b/packages/cli/src/commands/onboard/auth/gitlab/index.ts index 4fa77f6b02..9e2a674a9a 100644 --- a/packages/cli/src/commands/onboard/auth/gitlab/index.ts +++ b/packages/cli/src/commands/onboard/auth/gitlab/index.ts @@ -18,8 +18,8 @@ import chalk from 'chalk'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; import { Task } from '../../../../lib/tasks'; -import { addUserEntity, updateConfigFile } from '../config'; -import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files'; +import { addUserEntity, updateConfigFile } from '../../config'; +import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../../files'; import { patch } from '../patch'; const getConfig = (answers: Answers) => { diff --git a/packages/cli/src/commands/onboard/auth/patch.ts b/packages/cli/src/commands/onboard/auth/patch.ts index 2ebca2aab2..d004eeb157 100644 --- a/packages/cli/src/commands/onboard/auth/patch.ts +++ b/packages/cli/src/commands/onboard/auth/patch.ts @@ -17,7 +17,7 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import * as differ from 'diff'; -import { PATCH_FOLDER } from './files'; +import { PATCH_FOLDER } from '../files'; import { findPaths } from '@backstage/cli-common'; /* eslint-disable-next-line no-restricted-syntax */ diff --git a/packages/cli/src/commands/onboard/auth/config.ts b/packages/cli/src/commands/onboard/config.ts similarity index 68% rename from packages/cli/src/commands/onboard/auth/config.ts rename to packages/cli/src/commands/onboard/config.ts index 01afc5f96c..b5a2336a0a 100644 --- a/packages/cli/src/commands/onboard/auth/config.ts +++ b/packages/cli/src/commands/onboard/config.ts @@ -18,44 +18,14 @@ import { UserEntity } from '@backstage/catalog-model'; import * as fs from 'fs-extra'; import yaml from 'yaml'; -type AuthConfig = { - auth: { - providers: { - [key: string]: { - development: { - clientId: string; - clientSecret: string; - enterpriseInstanceUrl?: string; - audience?: string; - }; - }; - }; - }; -}; - -const catalogUserLocation = { - catalog: { - locations: [ - { - type: 'file', - target: '../../user-info.yaml', - rules: [{ allow: ['User'] }], - }, - ], - }, -}; - const readYaml = async (file: string) => { return yaml.parse(await fs.readFile(file, 'utf8')); }; -export const updateConfigFile = async ( - file: string, - authConfig: AuthConfig, -) => { +export const updateConfigFile = async (file: string, config: T) => { const content = fs.existsSync(file) - ? { ...(await readYaml(file)), ...authConfig, ...catalogUserLocation } - : { ...authConfig, ...catalogUserLocation }; + ? { ...(await readYaml(file)), ...config } + : { ...config }; return await fs.writeFile( file, diff --git a/packages/cli/src/commands/onboard/auth/files.ts b/packages/cli/src/commands/onboard/files.ts similarity index 100% rename from packages/cli/src/commands/onboard/auth/files.ts rename to packages/cli/src/commands/onboard/files.ts diff --git a/packages/cli/src/commands/onboard/integrations/github/index.ts b/packages/cli/src/commands/onboard/integrations/github/index.ts index 73416699e4..1a0d7a5ef5 100644 --- a/packages/cli/src/commands/onboard/integrations/github/index.ts +++ b/packages/cli/src/commands/onboard/integrations/github/index.ts @@ -17,37 +17,82 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../../../lib/tasks'; +import { updateConfigFile } from '../../config'; +import { APP_CONFIG_FILE } from '../../files'; + +type Answers = { + isEnterprise: boolean; + host: string; + apiBaseUrl?: string; + token: string; +}; + +const getConfig = ({ isEnterprise, host, apiBaseUrl, token }: Answers) => ({ + integrations: { + github: isEnterprise + ? [ + { + host, + apiBaseUrl, + token, + }, + ] + : [ + { + host, + token, + }, + ], + }, +}); export const github = async () => { - const host = 'https://github.com'; - const answers = await inquirer.prompt<{ hasEnterprise: boolean }>([ + let host = 'github.com'; + let apiBaseUrl: string | undefined; + + // TODO(tudi2d): Is GitHub Enterprise a valid setup if there is no Authentication? + const { isEnterprise } = await inquirer.prompt<{ + isEnterprise: Answers['isEnterprise']; + }>([ { type: 'confirm', - name: 'hasEnterprise', + name: 'isEnterprise', message: 'Are you using GitHub Enterprise?', }, ]); - if (answers.hasEnterprise) { - await inquirer.prompt<{ hasEnterprise: boolean }>([ - { - type: 'confirm', - name: 'hasEnterprise', - message: 'Are you using GitHub Enterprise?', - }, - { - type: 'input', - name: 'enterpriseInstanceUrl', - message: 'What is your GitHub Enterprise URL (e.g. ghe.example.net)?', - when: ({ hasEnterprise }) => hasEnterprise, - validate: (input: string) => Boolean(new URL(input)), - }, - ]); + if (isEnterprise) { + try { + const answers = await inquirer.prompt< + Pick + >([ + { + type: 'input', + name: 'apiBaseUrl', + message: + 'What is your GitHub Enterprise REST API URL (e.g. https://ghe.example.net/api/v3)?', + // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up + validate: (input: string) => Boolean(new URL(input)), + }, + { + type: 'input', + name: 'host', + message: + 'What is your GitHub Enterprise Host (e.g. ghe.example.net)?', + // TODO(tudi2d): validate: Must the host be part of the REST API URL? + }, + ]); + + host = answers.host; + apiBaseUrl = answers.apiBaseUrl; + } catch (err) { + return; + } } Task.log(` To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk.blue( - `${host}/settings/tokens/new', + `https://${host}/settings/tokens/new`, )} Select the following scopes: @@ -68,6 +113,32 @@ export const github = async () => { You can find the full documentation page here: ${chalk.blue( 'https://backstage.io/docs/integrations/github/locations', )} - `, - )}`); + `); + + const { token } = await inquirer.prompt<{ + token: Answers['token']; + }>([ + { + type: 'input', + name: 'token', + message: + 'Please insert your personal access token to setup the GitHub Integration', + // TODO(tudi2d): validate: Must the host be part of the REST API URL? + }, + ]); + + const config = getConfig({ + token, + isEnterprise, + host, + apiBaseUrl, + }); + + Task.log('Setting up GitHub Integration for you...'); + + await Task.forItem( + 'Updating', + APP_CONFIG_FILE, + async () => await updateConfigFile(APP_CONFIG_FILE, config), + ); }; diff --git a/packages/cli/src/commands/onboard/integrations/index.ts b/packages/cli/src/commands/onboard/integrations/index.ts index 2b55ab2bba..f3ac6754d6 100644 --- a/packages/cli/src/commands/onboard/integrations/index.ts +++ b/packages/cli/src/commands/onboard/integrations/index.ts @@ -29,7 +29,7 @@ export async function integrations(): Promise { integration?: Integration; }>([ { - // TODO(tudi2d): Should be multiple choice + // TODO(tudi2d): Let's start with one, but it should be multiple choice in the future type: 'list', name: 'integration', message: 'Please select an integration:', From 3d121668268fd1f1494d3d4471e055e50cdd8190 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 6 Mar 2023 13:14:37 +0100 Subject: [PATCH 28/34] Chain Scaffolder inquirer prompts Signed-off-by: Philipp Hugenroth --- .../src/commands/onboard/auth/gitlab/index.ts | 2 +- .../onboard/integrations/github/index.ts | 54 +++++++++---------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/cli/src/commands/onboard/auth/gitlab/index.ts b/packages/cli/src/commands/onboard/auth/gitlab/index.ts index 9e2a674a9a..702537fe70 100644 --- a/packages/cli/src/commands/onboard/auth/gitlab/index.ts +++ b/packages/cli/src/commands/onboard/auth/gitlab/index.ts @@ -52,7 +52,7 @@ type Answers = { export const gitlab = async () => { Task.log(` - To add GitLub authentication, you must create an Application from the GitLab Settings: ${chalk.blue( + To add GitLab authentication, you must create an Application from the GitLab Settings: ${chalk.blue( 'https://gitlab.com/-/profile/applications', )} The Redirect URI should point to your Backstage backend auth handler. diff --git a/packages/cli/src/commands/onboard/integrations/github/index.ts b/packages/cli/src/commands/onboard/integrations/github/index.ts index 1a0d7a5ef5..ac4fd37406 100644 --- a/packages/cli/src/commands/onboard/integrations/github/index.ts +++ b/packages/cli/src/commands/onboard/integrations/github/index.ts @@ -48,46 +48,42 @@ const getConfig = ({ isEnterprise, host, apiBaseUrl, token }: Answers) => ({ export const github = async () => { let host = 'github.com'; - let apiBaseUrl: string | undefined; // TODO(tudi2d): Is GitHub Enterprise a valid setup if there is no Authentication? - const { isEnterprise } = await inquirer.prompt<{ + const { + isEnterprise, + host: _host, + apiBaseUrl, + } = await inquirer.prompt<{ isEnterprise: Answers['isEnterprise']; + apiBaseUrl: Answers['apiBaseUrl']; + host: Answers['host']; }>([ { type: 'confirm', name: 'isEnterprise', message: 'Are you using GitHub Enterprise?', }, + { + type: 'input', + name: 'apiBaseUrl', + when: ({ isEnterprise: _isEnterprise }) => _isEnterprise, + message: + 'What is your GitHub Enterprise REST API URL (e.g. https://ghe.example.net/api/v3)?', + // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up + validate: (input: string) => Boolean(new URL(input)), + }, + { + type: 'input', + name: 'host', + when: ({ isEnterprise: _isEnterprise }) => _isEnterprise, + message: 'What is your GitHub Enterprise Host (e.g. ghe.example.net)?', + // TODO(tudi2d): validate: Must the host be part of the REST API URL? + }, ]); if (isEnterprise) { - try { - const answers = await inquirer.prompt< - Pick - >([ - { - type: 'input', - name: 'apiBaseUrl', - message: - 'What is your GitHub Enterprise REST API URL (e.g. https://ghe.example.net/api/v3)?', - // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up - validate: (input: string) => Boolean(new URL(input)), - }, - { - type: 'input', - name: 'host', - message: - 'What is your GitHub Enterprise Host (e.g. ghe.example.net)?', - // TODO(tudi2d): validate: Must the host be part of the REST API URL? - }, - ]); - - host = answers.host; - apiBaseUrl = answers.apiBaseUrl; - } catch (err) { - return; - } + host = _host; } Task.log(` @@ -123,7 +119,7 @@ export const github = async () => { name: 'token', message: 'Please insert your personal access token to setup the GitHub Integration', - // TODO(tudi2d): validate: Must the host be part of the REST API URL? + // TODO(tudi2d): validate }, ]); From 8e254c06bc3655bc4ef93c76bbb727fd50f6959e Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 8 Mar 2023 09:48:47 +0100 Subject: [PATCH 29/34] cli: Use custom resolver to sign-in as guest instead, do not add user-info to catalog. Signed-off-by: Marcus Eide --- .../src/commands/onboard/auth/github/index.ts | 46 ++----------------- .../src/commands/onboard/auth/gitlab/index.ts | 17 +------ .../02-github.Use-signIn-resolver.patch | 14 ------ ...e.patch => 02-gitlab.Add-SignInPage.patch} | 0 .../03-gitlab.Change-signIn-resolver.patch | 5 ++ .../04-gitlab.Use-signIn-resolver.patch | 27 ----------- packages/cli/src/commands/onboard/config.ts | 27 ----------- packages/cli/src/commands/onboard/files.ts | 1 - 8 files changed, 11 insertions(+), 126 deletions(-) delete mode 100644 packages/cli/src/commands/onboard/auth/patches/02-github.Use-signIn-resolver.patch rename packages/cli/src/commands/onboard/auth/patches/{03-gitlab.Add-SignInPage.patch => 02-gitlab.Add-SignInPage.patch} (100%) create mode 100644 packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch delete mode 100644 packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch diff --git a/packages/cli/src/commands/onboard/auth/github/index.ts b/packages/cli/src/commands/onboard/auth/github/index.ts index ce53d23ad3..7e1e63a8fe 100644 --- a/packages/cli/src/commands/onboard/auth/github/index.ts +++ b/packages/cli/src/commands/onboard/auth/github/index.ts @@ -18,32 +18,18 @@ import { OAuthApp } from '@octokit/oauth-app'; import chalk from 'chalk'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; -import fetch from 'node-fetch'; import { Task } from '../../../../lib/tasks'; -import { addUserEntity, updateConfigFile } from '../../config'; -import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../../files'; +import { updateConfigFile } from '../../config'; +import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files'; import { patch } from '../patch'; type Answers = { - username: string; clientSecret: string; clientId: string; hasEnterprise: boolean; enterpriseInstanceUrl?: string; }; -const catalogUserLocation = { - catalog: { - locations: [ - { - type: 'file', - target: '../../user-info.yaml', - rules: [{ allow: ['User'] }], - }, - ], - }, -}; - const validateCredentials = async (clientId: string, clientSecret: string) => { try { const app = new OAuthApp({ @@ -108,18 +94,6 @@ export const github = async () => { `); const answers = await inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'What is your GitHub username?', - validate: async (input: string) => { - const response = await fetch(`https://api.github.com/users/${input}`); - if (!response.ok) { - return chalk.red('Unknown user. Please try again.'); - } - return true; - }, - }, { type: 'input', name: 'clientId', @@ -146,7 +120,7 @@ export const github = async () => { }, ]); - const { username, clientId, clientSecret } = answers; + const { clientId, clientSecret } = answers; const config = getConfig(answers); Task.log('Setting up GitHub Authentication for you...'); @@ -159,19 +133,7 @@ export const github = async () => { await Task.forItem( 'Updating', APP_CONFIG_FILE, - async () => - await updateConfigFile(APP_CONFIG_FILE, { - ...config, - ...catalogUserLocation, - }), - ); - await Task.forItem( - 'Creating', - USER_ENTITY_FILE, - async () => - await addUserEntity(USER_ENTITY_FILE, username, { - 'github.com/user-login': username, - }), + async () => await updateConfigFile(APP_CONFIG_FILE, config), ); const patches = await fs.readdir(PATCH_FOLDER); diff --git a/packages/cli/src/commands/onboard/auth/gitlab/index.ts b/packages/cli/src/commands/onboard/auth/gitlab/index.ts index 702537fe70..1940f66017 100644 --- a/packages/cli/src/commands/onboard/auth/gitlab/index.ts +++ b/packages/cli/src/commands/onboard/auth/gitlab/index.ts @@ -18,8 +18,8 @@ import chalk from 'chalk'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; import { Task } from '../../../../lib/tasks'; -import { addUserEntity, updateConfigFile } from '../../config'; -import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../../files'; +import { updateConfigFile } from '../../config'; +import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files'; import { patch } from '../patch'; const getConfig = (answers: Answers) => { @@ -43,7 +43,6 @@ const getConfig = (answers: Answers) => { }; type Answers = { - username: string; clientSecret: string; clientId: string; hasAudience: boolean; @@ -69,12 +68,6 @@ export const gitlab = async () => { `); const answers = await inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'What is your GitLab username?', - validate: (input: string) => (input.length ? true : false), - }, { type: 'input', name: 'clientId', @@ -101,7 +94,6 @@ export const gitlab = async () => { }, ]); - const { username } = answers; const config = getConfig(answers); Task.log('Setting up GitLab Authentication for you...'); @@ -111,11 +103,6 @@ export const gitlab = async () => { APP_CONFIG_FILE, async () => await updateConfigFile(APP_CONFIG_FILE, config), ); - await Task.forItem( - 'Creating', - USER_ENTITY_FILE, - async () => await addUserEntity(USER_ENTITY_FILE, username), - ); const patches = await fs.readdir(PATCH_FOLDER); for (const patchFile of patches.filter(p => p.includes('gitlab'))) { diff --git a/packages/cli/src/commands/onboard/auth/patches/02-github.Use-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/02-github.Use-signIn-resolver.patch deleted file mode 100644 index 2baaff7cf9..0000000000 --- a/packages/cli/src/commands/onboard/auth/patches/02-github.Use-signIn-resolver.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- a/packages/backend/src/plugins/auth.ts -+++ b/packages/backend/src/plugins/auth.ts -@@ -40,10 +40 @@ export default async function createPlugin( -- 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(), -+ resolver: providers.github.resolvers.usernameMatchingUserEntityName(), \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch similarity index 100% rename from packages/cli/src/commands/onboard/auth/patches/03-gitlab.Add-SignInPage.patch rename to packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch diff --git a/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch new file mode 100644 index 0000000000..7a69669674 --- /dev/null +++ b/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch @@ -0,0 +1,5 @@ +--- a/packages/backend/src/plugins/auth.ts ++++ b/packages/backend/src/plugins/auth.ts +@@ -38 +38 @@ export default async function createPlugin( +- github: providers.github.create({ ++ gitlab: providers.gitlab.create({ \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch deleted file mode 100644 index 04b432c179..0000000000 --- a/packages/cli/src/commands/onboard/auth/patches/04-gitlab.Use-signIn-resolver.patch +++ /dev/null @@ -1,27 +0,0 @@ ---- a/packages/backend/src/plugins/auth.ts -+++ b/packages/backend/src/plugins/auth.ts -@@ -38 +38 @@ export default async function createPlugin( -- github: providers.github.create({ -+ gitlab: providers.gitlab.create({ -@@ -40,8 +40,11 @@ export default async function createPlugin( -- 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 -- }, -- }); -+ async resolver(info, ctx) { -+ const { fullProfile } = info.result; -+ -+ const userId = fullProfile.username; -+ if (!userId) { -+ throw new Error( -+ `GitLab user profile does not contain a username`, -+ ); -+ } -+ -+ return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); -@@ -49 +51,0 @@ export default async function createPlugin( -- // resolver: providers.github.resolvers.usernameMatchingUserEntityName(), \ No newline at end of file diff --git a/packages/cli/src/commands/onboard/config.ts b/packages/cli/src/commands/onboard/config.ts index b5a2336a0a..8d419dd71a 100644 --- a/packages/cli/src/commands/onboard/config.ts +++ b/packages/cli/src/commands/onboard/config.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { UserEntity } from '@backstage/catalog-model'; import * as fs from 'fs-extra'; import yaml from 'yaml'; @@ -35,29 +34,3 @@ export const updateConfigFile = async (file: string, config: T) => { 'utf8', ); }; - -export const addUserEntity = async ( - file: string, - username: string, - annotations?: Record, -) => { - const content: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: username, - annotations: { ...annotations }, - }, - spec: { - memberOf: [], - }, - }; - - return await fs.writeFile( - file, - yaml.stringify(content, { - indent: 2, - }), - 'utf8', - ); -}; diff --git a/packages/cli/src/commands/onboard/files.ts b/packages/cli/src/commands/onboard/files.ts index af07ef254b..17f8a0f85e 100644 --- a/packages/cli/src/commands/onboard/files.ts +++ b/packages/cli/src/commands/onboard/files.ts @@ -20,7 +20,6 @@ import * as path from 'path'; /* eslint-disable-next-line no-restricted-syntax */ const { targetRoot, ownDir } = findPaths(__dirname); export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml'); -export const USER_ENTITY_FILE = path.join(targetRoot, 'user-info.yaml'); export const PATCH_FOLDER = path.join( ownDir, 'src', From b590c95ed8e64705e3a68e4b37afda9da9249a33 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 8 Mar 2023 16:00:42 +0100 Subject: [PATCH 30/34] cli: Remember provider selection from auth setup when setting up software templates Signed-off-by: Marcus Eide --- .../src/commands/onboard/auth/github/index.ts | 6 +- .../src/commands/onboard/auth/gitlab/index.ts | 6 +- .../cli/src/commands/onboard/auth/index.ts | 32 +++--- packages/cli/src/commands/onboard/command.ts | 87 ++++++++------- .../onboard/integrations/github/index.ts | 100 +++++++++--------- .../commands/onboard/integrations/index.ts | 72 +++++++++---- 6 files changed, 167 insertions(+), 136 deletions(-) diff --git a/packages/cli/src/commands/onboard/auth/github/index.ts b/packages/cli/src/commands/onboard/auth/github/index.ts index 7e1e63a8fe..36c6b02038 100644 --- a/packages/cli/src/commands/onboard/auth/github/index.ts +++ b/packages/cli/src/commands/onboard/auth/github/index.ts @@ -23,7 +23,7 @@ import { updateConfigFile } from '../../config'; import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files'; import { patch } from '../patch'; -type Answers = { +export type Answers = { clientSecret: string; clientId: string; hasEnterprise: boolean; @@ -76,7 +76,7 @@ const getConfig = (answers: Answers) => { }; }; -export const github = async () => { +export const github = async (): Promise => { Task.log(` To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue( 'https://github.com/settings/developers', @@ -142,4 +142,6 @@ export const github = async () => { await patch(patchFile); }); } + + return answers; }; diff --git a/packages/cli/src/commands/onboard/auth/gitlab/index.ts b/packages/cli/src/commands/onboard/auth/gitlab/index.ts index 1940f66017..bc5491111c 100644 --- a/packages/cli/src/commands/onboard/auth/gitlab/index.ts +++ b/packages/cli/src/commands/onboard/auth/gitlab/index.ts @@ -42,14 +42,14 @@ const getConfig = (answers: Answers) => { }; }; -type Answers = { +export type Answers = { clientSecret: string; clientId: string; hasAudience: boolean; audience?: string; }; -export const gitlab = async () => { +export const gitlab = async (): Promise => { Task.log(` To add GitLab authentication, you must create an Application from the GitLab Settings: ${chalk.blue( 'https://gitlab.com/-/profile/applications', @@ -110,4 +110,6 @@ export const gitlab = async () => { await patch(patchFile); }); } + + return answers; }; diff --git a/packages/cli/src/commands/onboard/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts index df9471e1db..5a299e2fb5 100644 --- a/packages/cli/src/commands/onboard/auth/index.ts +++ b/packages/cli/src/commands/onboard/auth/index.ts @@ -14,44 +14,50 @@ * limitations under the License. */ -import chalk from 'chalk'; import inquirer from 'inquirer'; import { Task } from '../../../lib/tasks'; -import { github } from './github'; -import { gitlab } from './gitlab'; +import { github, Answers as GitHubAnswers } from './github'; +import { gitlab, Answers as GitLabAnswers } from './gitlab'; -export async function auth(): Promise { +export { type GitHubAnswers, type GitLabAnswers }; + +export async function auth(): Promise<{ + provider: string; + answers: GitHubAnswers | GitLabAnswers; +}> { const answers = await inquirer.prompt<{ - provider?: string; + provider: string; }>([ { type: 'list', name: 'provider', - message: 'Please select a provider:', + message: 'Please select an authentication provider:', choices: ['GitHub', 'GitLab'], }, ]); const { provider } = answers; + let providerAnswers; switch (provider) { case 'GitHub': { - await github(); + providerAnswers = await github(); break; } case 'GitLab': { - await gitlab(); + providerAnswers = await gitlab(); break; } default: throw new Error(`Provider ${provider} not implemented yet.`); } + Task.log(); Task.log(`Done setting up ${provider} Authentication!`); Task.log(); - Task.log( - `You can now start your app with ${chalk.inverse( - chalk.italic('yarn dev'), - )}`, - ); + + return { + provider, + answers: providerAnswers, + }; } diff --git a/packages/cli/src/commands/onboard/command.ts b/packages/cli/src/commands/onboard/command.ts index 1cfaabed12..5cf25953ab 100644 --- a/packages/cli/src/commands/onboard/command.ts +++ b/packages/cli/src/commands/onboard/command.ts @@ -16,55 +16,54 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; +import { Task } from '../../lib/tasks'; import { auth } from './auth'; import { integrations } from './integrations'; export async function command(): Promise { - try { - // TODO(tudi2d): Is there a way to make this cleaner? - const answers = await inquirer.prompt<{ - shouldSetupAuth: boolean; - }>([ - { - type: 'confirm', - name: 'shouldSetupAuth', - message: 'Do you want to set up Authentication for this project?', - default: true, - }, - ]); + const answers = await inquirer.prompt<{ + shouldSetupAuth: boolean; + shouldSetupScaffolder: boolean; + }>([ + { + type: 'confirm', + name: 'shouldSetupAuth', + message: 'Do you want to set up Authentication for this project?', + default: true, + }, + { + type: 'confirm', + name: 'shouldSetupScaffolder', + message: 'Do you want to use Software Templates in this project?', + default: true, + }, + ]); - if (!answers.shouldSetupAuth) { - console.log( - chalk.yellow( - 'If you change your mind, feel free to re-run this command.', - ), - ); - } else { - await auth(); - } + const { shouldSetupAuth, shouldSetupScaffolder } = answers; - // TODO(tudi2d): Select "Core Features" here such that we can determin if the integrations need to be setup for either Catalog, Scaffollder or both - await inquirer - .prompt<{ - setupScaffolder: boolean; - }>([ - { - type: 'confirm', - name: 'setupScaffolder', - message: 'Do you want to setup Sofware Templates?', - default: true, - }, - ]) - .then(async ({ setupScaffolder }) => - setupScaffolder - ? await integrations() - : console.log( - chalk.yellow( - 'If you change your mind, feel free to re-run this command.', - ), - ), - ); - } catch (err) { - process.exit(-1); + let providerInfo; + if (shouldSetupAuth) { + providerInfo = await auth(); } + + if (shouldSetupScaffolder) { + await integrations(providerInfo); + } + + if (!shouldSetupAuth && !shouldSetupScaffolder) { + Task.log( + chalk.yellow( + 'If you change your mind, feel free to re-run this command.', + ), + ); + return; + } + + Task.log(); + Task.log( + `You can now start your app with ${chalk.inverse( + chalk.italic('yarn dev'), + )}`, + ); + Task.log(); } diff --git a/packages/cli/src/commands/onboard/integrations/github/index.ts b/packages/cli/src/commands/onboard/integrations/github/index.ts index ac4fd37406..883a119c00 100644 --- a/packages/cli/src/commands/onboard/integrations/github/index.ts +++ b/packages/cli/src/commands/onboard/integrations/github/index.ts @@ -19,76 +19,74 @@ import inquirer from 'inquirer'; import { Task } from '../../../../lib/tasks'; import { updateConfigFile } from '../../config'; import { APP_CONFIG_FILE } from '../../files'; +import { GitHubAnswers } from '../../auth'; type Answers = { - isEnterprise: boolean; - host: string; - apiBaseUrl?: string; - token: string; + hasEnterprise: boolean; + enterpriseInstanceUrl: string; + apiBaseUrl: string; }; -const getConfig = ({ isEnterprise, host, apiBaseUrl, token }: Answers) => ({ +const getConfig = ({ + hasEnterprise, + apiBaseUrl, + host, + token, +}: { + hasEnterprise: boolean; + apiBaseUrl: string; + host: string; + token: string; +}) => ({ integrations: { - github: isEnterprise - ? [ - { - host, - apiBaseUrl, - token, - }, - ] - : [ - { - host, - token, - }, - ], + github: [ + { + host, + token, + ...(hasEnterprise && { + apiBaseUrl, + }), + }, + ], }, }); -export const github = async () => { - let host = 'github.com'; - +export const github = async (providerAnswers?: GitHubAnswers) => { // TODO(tudi2d): Is GitHub Enterprise a valid setup if there is no Authentication? - const { - isEnterprise, - host: _host, - apiBaseUrl, - } = await inquirer.prompt<{ - isEnterprise: Answers['isEnterprise']; - apiBaseUrl: Answers['apiBaseUrl']; - host: Answers['host']; - }>([ + const answers = await inquirer.prompt([ { type: 'confirm', - name: 'isEnterprise', + name: 'hasEnterprise', message: 'Are you using GitHub Enterprise?', + when: () => typeof providerAnswers === 'undefined', }, { type: 'input', - name: 'apiBaseUrl', - when: ({ isEnterprise: _isEnterprise }) => _isEnterprise, - message: - 'What is your GitHub Enterprise REST API URL (e.g. https://ghe.example.net/api/v3)?', - // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up + name: 'enterpriseInstanceUrl', + message: 'What is your URL for GitHub Enterprise?', + when: ({ hasEnterprise }) => hasEnterprise, validate: (input: string) => Boolean(new URL(input)), }, { type: 'input', - name: 'host', - when: ({ isEnterprise: _isEnterprise }) => _isEnterprise, - message: 'What is your GitHub Enterprise Host (e.g. ghe.example.net)?', - // TODO(tudi2d): validate: Must the host be part of the REST API URL? + name: 'apiBaseUrl', + message: 'What is your GitHub Enterprise API path?', + default: '/api/v3', + when: ({ hasEnterprise }) => + hasEnterprise || providerAnswers?.hasEnterprise, + // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up }, ]); - if (isEnterprise) { - host = _host; - } + const host = new URL( + providerAnswers?.enterpriseInstanceUrl ?? + answers?.enterpriseInstanceUrl ?? + 'http://github.com', + ); Task.log(` To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk.blue( - `https://${host}/settings/tokens/new`, + `${host.origin}/settings/tokens/new`, )} Select the following scopes: @@ -111,9 +109,7 @@ export const github = async () => { )} `); - const { token } = await inquirer.prompt<{ - token: Answers['token']; - }>([ + const { token } = await inquirer.prompt<{ token: string }>([ { type: 'input', name: 'token', @@ -124,13 +120,13 @@ export const github = async () => { ]); const config = getConfig({ + hasEnterprise: providerAnswers?.hasEnterprise ?? answers.hasEnterprise, + apiBaseUrl: host.origin + answers.apiBaseUrl, + host: host.hostname, token, - isEnterprise, - host, - apiBaseUrl, }); - Task.log('Setting up GitHub Integration for you...'); + Task.log('Setting up Software Templates using GitHub integration for you...'); await Task.forItem( 'Updating', diff --git a/packages/cli/src/commands/onboard/integrations/index.ts b/packages/cli/src/commands/onboard/integrations/index.ts index f3ac6754d6..5cb2f95b38 100644 --- a/packages/cli/src/commands/onboard/integrations/index.ts +++ b/packages/cli/src/commands/onboard/integrations/index.ts @@ -15,6 +15,8 @@ */ import inquirer from 'inquirer'; +import { Task } from '../../../lib/tasks'; +import { GitHubAnswers, GitLabAnswers } from '../auth'; import { github } from './github'; enum Integration { @@ -23,27 +25,51 @@ enum Integration { const Integrations: Integration[] = [Integration.GITHUB]; -export async function integrations(): Promise { - await inquirer - .prompt<{ - integration?: Integration; - }>([ - { - // TODO(tudi2d): Let's start with one, but it should be multiple choice in the future - type: 'list', - name: 'integration', - message: 'Please select an integration:', - choices: Integrations, - }, - ]) - .then(async ({ integration }) => { - if (integration) { - switch (integration) { - case Integration.GITHUB: - await github(); - break; - default: - } - } - }); +export async function integrations(providerInfo?: { + provider: string; + answers: GitHubAnswers | GitLabAnswers; +}): Promise { + const answers = await inquirer.prompt<{ + integration?: Integration; + shouldUsePreviousProvider: boolean; + }>([ + { + type: 'confirm', + name: 'shouldUsePreviousProvider', + message: `Do you want to keep using ${providerInfo?.provider} as your provider when setting up Software Templates?`, + when: () => + providerInfo?.provider && + Object.values(Integrations).includes( + providerInfo!.provider as Integration, + ), + }, + { + // TODO(tudi2d): Let's start with one, but it should be multiple choice in the future + type: 'list', + name: 'integration', + message: 'Please select an integration provider:', + choices: Integrations, + when: ({ shouldUsePreviousProvider }) => !shouldUsePreviousProvider, + }, + ]); + + if (answers.shouldUsePreviousProvider) { + answers.integration = providerInfo!.provider as Integration; + } + + switch (answers.integration) { + case Integration.GITHUB: { + const providerAnswers = + providerInfo?.provider === 'GitHub' + ? (providerInfo!.answers as GitHubAnswers) + : undefined; + await github(providerAnswers); + break; + } + default: + } + + Task.log(); + Task.log(`Done setting up ${answers.integration} Integration!`); + Task.log(); } From 349b70af35962d1f99b544e09eeda6a346f340e7 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Thu, 9 Mar 2023 16:53:10 +0100 Subject: [PATCH 31/34] Update packages/cli/src/commands/onboard/README.md Co-authored-by: Patrik Oldsberg Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/onboard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/onboard/README.md b/packages/cli/src/commands/onboard/README.md index d2a6e10256..71e51a2077 100644 --- a/packages/cli/src/commands/onboard/README.md +++ b/packages/cli/src/commands/onboard/README.md @@ -14,7 +14,7 @@ npx @backstage/create-app cd new-backstage-app # Run the modded cli in the new app -../github/backstage/packages/cli/bin/backstage-cli admin +../github/backstage/packages/cli/bin/backstage-cli onboard ? Do you want to set up Authentication for this project? (Y/n) ... From 3c5b436237a23f82ef09ad574b02720115bb9451 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 14 Mar 2023 11:13:41 +0100 Subject: [PATCH 32/34] Adjust PR to review - Add comment in local config indicating override - Restructure files & folders to avoid folders with only one index file Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/onboard/README.md | 13 ++++----- .../cli/src/commands/onboard/auth/index.ts | 4 +-- .../{github/index.ts => providers/github.ts} | 0 .../{gitlab/index.ts => providers/gitlab.ts} | 0 packages/cli/src/commands/onboard/config.ts | 29 ++++++++++++------- .../{github/index.ts => github.ts} | 8 ++--- 6 files changed, 31 insertions(+), 23 deletions(-) rename packages/cli/src/commands/onboard/auth/{github/index.ts => providers/github.ts} (100%) rename packages/cli/src/commands/onboard/auth/{gitlab/index.ts => providers/gitlab.ts} (100%) rename packages/cli/src/commands/onboard/integrations/{github/index.ts => github.ts} (95%) diff --git a/packages/cli/src/commands/onboard/README.md b/packages/cli/src/commands/onboard/README.md index 71e51a2077..8ab2d5f795 100644 --- a/packages/cli/src/commands/onboard/README.md +++ b/packages/cli/src/commands/onboard/README.md @@ -2,22 +2,21 @@ ## Local dev -```bash -# Checkout the branch -cd backstage -git fetch -git checkout goa/cli-admin-command - # Create a new app + +```bash cd .. npx @backstage/create-app cd new-backstage-app # Run the modded cli in the new app -../github/backstage/packages/cli/bin/backstage-cli onboard + +../RELATIVE_PATH_TO_PROJECT/backstage/packages/cli/bin/backstage-cli onboard ? Do you want to set up Authentication for this project? (Y/n) ... # Try the app with the new changes + yarn dev + ``` diff --git a/packages/cli/src/commands/onboard/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts index 5a299e2fb5..0f4537b110 100644 --- a/packages/cli/src/commands/onboard/auth/index.ts +++ b/packages/cli/src/commands/onboard/auth/index.ts @@ -16,8 +16,8 @@ import inquirer from 'inquirer'; import { Task } from '../../../lib/tasks'; -import { github, Answers as GitHubAnswers } from './github'; -import { gitlab, Answers as GitLabAnswers } from './gitlab'; +import { github, Answers as GitHubAnswers } from './providers/github'; +import { gitlab, Answers as GitLabAnswers } from './providers/gitlab'; export { type GitHubAnswers, type GitLabAnswers }; diff --git a/packages/cli/src/commands/onboard/auth/github/index.ts b/packages/cli/src/commands/onboard/auth/providers/github.ts similarity index 100% rename from packages/cli/src/commands/onboard/auth/github/index.ts rename to packages/cli/src/commands/onboard/auth/providers/github.ts diff --git a/packages/cli/src/commands/onboard/auth/gitlab/index.ts b/packages/cli/src/commands/onboard/auth/providers/gitlab.ts similarity index 100% rename from packages/cli/src/commands/onboard/auth/gitlab/index.ts rename to packages/cli/src/commands/onboard/auth/providers/gitlab.ts diff --git a/packages/cli/src/commands/onboard/config.ts b/packages/cli/src/commands/onboard/config.ts index 8d419dd71a..cce3119fcc 100644 --- a/packages/cli/src/commands/onboard/config.ts +++ b/packages/cli/src/commands/onboard/config.ts @@ -22,15 +22,24 @@ const readYaml = async (file: string) => { }; export const updateConfigFile = async (file: string, config: T) => { - const content = fs.existsSync(file) - ? { ...(await readYaml(file)), ...config } - : { ...config }; + const staticContent = + '# Backstage override configuration for your local development environment \n'; - return await fs.writeFile( - file, - yaml.stringify(content, { - indent: 2, - }), - 'utf8', - ); + const content = fs.existsSync(file) + ? yaml.stringify( + { ...(await readYaml(file)), ...config }, + { + indent: 2, + }, + ) + : staticContent.concat( + yaml.stringify( + { ...config }, + { + indent: 2, + }, + ), + ); + + return await fs.writeFile(file, content, 'utf8'); }; diff --git a/packages/cli/src/commands/onboard/integrations/github/index.ts b/packages/cli/src/commands/onboard/integrations/github.ts similarity index 95% rename from packages/cli/src/commands/onboard/integrations/github/index.ts rename to packages/cli/src/commands/onboard/integrations/github.ts index 883a119c00..a0538c7ace 100644 --- a/packages/cli/src/commands/onboard/integrations/github/index.ts +++ b/packages/cli/src/commands/onboard/integrations/github.ts @@ -16,10 +16,10 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; -import { Task } from '../../../../lib/tasks'; -import { updateConfigFile } from '../../config'; -import { APP_CONFIG_FILE } from '../../files'; -import { GitHubAnswers } from '../../auth'; +import { Task } from '../../../lib/tasks'; +import { updateConfigFile } from '../config'; +import { APP_CONFIG_FILE } from '../files'; +import { GitHubAnswers } from '../auth'; type Answers = { hasEnterprise: boolean; From 3382cafa9e5ad0a38b8f91170bf241bbdcf2d639 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 15 Mar 2023 12:27:03 +0100 Subject: [PATCH 33/34] Set @backstage/sharks to CODEOWNERS for CLI onboard command Signed-off-by: Philipp Hugenroth --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 010af451af..4d375a76f2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ yarn.lock @backstage/maintainers @back /docs/features/search @backstage/techdocs-core /docs/features/techdocs @backstage/techdocs-core /docs/plugins/integrating-search-into-plugins.md @backstage/techdocs-core +/packages/cli/src/commands/onboard @backstage/sharks /packages/techdocs-cli @backstage/techdocs-core /packages/techdocs-cli-embedded-app @backstage/techdocs-core /plugins/adr @backstage/maintainers @kuangp From 6b23db2b811a1e813fb088621ede07818bbc6073 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 15 Mar 2023 15:09:03 +0100 Subject: [PATCH 34/34] Update packages/cli/src/commands/onboard/README.md Co-authored-by: Johan Haals Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/onboard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/onboard/README.md b/packages/cli/src/commands/onboard/README.md index 8ab2d5f795..0fb4d22666 100644 --- a/packages/cli/src/commands/onboard/README.md +++ b/packages/cli/src/commands/onboard/README.md @@ -1,4 +1,4 @@ -# admin tool +# onboard ## Local dev