From 67c693c6b2b38f7131ae1a4f942f94d89bae8f4c Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 16 Feb 2023 13:50:56 +0100 Subject: [PATCH] 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'); +};