cli: Add ability to patch existing files

Signed-off-by: Marcus Eide <eide@spotify.com>
This commit is contained in:
Marcus Eide
2023-02-16 13:50:56 +01:00
committed by Philipp Hugenroth
parent e3e0b14ec4
commit 67c693c6b2
7 changed files with 184 additions and 35 deletions
@@ -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');
};
@@ -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',
);
@@ -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,
);
@@ -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' +
'+ <SignInPage\n' +
'+ {...props}\n' +
'+ auto\n' +
'+ provider={{\n' +
"+ id: 'github-auth-provider',\n" +
"+ title: 'GitHub',\n" +
"+ message: 'Sign in using GitHub',\n" +
'+ apiRef: githubAuthApiRef,\n' +
'+ }}\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';
@@ -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;
}
@@ -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);
};
@@ -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');
};