UNPKG

@backstage/cli

Version:

CLI for developing Backstage plugins and apps

472 lines (448 loc) • 14.4 kB
'use strict'; var chalk = require('chalk'); var inquirer = require('inquirer'); var tasks = require('./tasks-84de240c.cjs.js'); var oauthApp = require('@octokit/oauth-app'); var fs = require('fs-extra'); var yaml = require('yaml'); var cliCommon = require('@backstage/cli-common'); var path = require('path'); var differ = require('diff'); require('handlebars'); require('ora'); require('util'); require('recursive-readdir'); require('child_process'); require('@backstage/errors'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk); var inquirer__default = /*#__PURE__*/_interopDefaultLegacy(inquirer); var fs__namespace = /*#__PURE__*/_interopNamespace(fs); var yaml__default = /*#__PURE__*/_interopDefaultLegacy(yaml); var path__namespace = /*#__PURE__*/_interopNamespace(path); var differ__namespace = /*#__PURE__*/_interopNamespace(differ); const readYaml = async (file) => { return yaml__default["default"].parse(await fs__namespace.readFile(file, "utf8")); }; const updateConfigFile = async (file, config) => { const staticContent = "# Backstage override configuration for your local development environment \n"; const content = fs__namespace.existsSync(file) ? yaml__default["default"].stringify( { ...await readYaml(file), ...config }, { indent: 2 } ) : staticContent.concat( yaml__default["default"].stringify( { ...config }, { indent: 2 } ) ); return await fs__namespace.writeFile(file, content, "utf8"); }; const { targetRoot: targetRoot$1, ownDir } = cliCommon.findPaths(__dirname); const APP_CONFIG_FILE = path__namespace.join(targetRoot$1, "app-config.local.yaml"); const PATCH_FOLDER = path__namespace.join( ownDir, "src", "commands", "onboard", "auth", "patches" ); const { targetRoot } = cliCommon.findPaths(__dirname); const patch = async (patchFile) => { const patchContent = await fs__namespace.readFile( path__namespace.join(PATCH_FOLDER, patchFile), "utf8" ); const targetName = patchContent.split("\n")[0].replace("--- a", ""); const targetFile = path__namespace.join(targetRoot, targetName); const oldContent = await fs__namespace.readFile(targetFile, "utf8"); const newContent = differ__namespace.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__namespace.writeFile(targetFile, newContent, "utf8"); }; const validateCredentials = async (clientId, clientSecret) => { try { const app = new oauthApp.OAuthApp({ clientId, clientSecret }); await app.createToken({ code: "%NOT-VALID-CODE%" }); } catch (error) { if (error.response.status !== 200 && error.response.data.error !== "bad_verification_code") { throw new Error(`Validating GitHub Credentials failed.`); } } }; const getConfig$2 = (answers) => { const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } = answers; return { auth: { providers: { github: { development: { clientId, clientSecret, ...hasEnterprise && { enterpriseInstanceUrl } } } } } }; }; const github$1 = async () => { tasks.Task.log(` To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk__default["default"].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__default["default"].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__default["default"].blue( "https://backstage.io/docs/auth/github/provider" )} `); const answers = await inquirer__default["default"].prompt([ { type: "input", name: "clientId", message: "What is your Client Id?", validate: (input) => input.length ? true : false }, { type: "input", name: "clientSecret", message: "What is your Client Secret?", validate: (input) => 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) => Boolean(new URL(input)) } ]); const { clientId, clientSecret } = answers; const config = getConfig$2(answers); tasks.Task.log("Setting up GitHub Authentication for you..."); await tasks.Task.forItem( "Validating", "credentials", async () => await validateCredentials(clientId, clientSecret) ); await tasks.Task.forItem( "Updating", APP_CONFIG_FILE, async () => await updateConfigFile(APP_CONFIG_FILE, config) ); const patches = await fs__namespace.readdir(PATCH_FOLDER); for (const patchFile of patches.filter((p) => p.includes("github"))) { await tasks.Task.forItem("Patching", patchFile, async () => { await patch(patchFile); }); } return answers; }; const getConfig$1 = (answers) => { const { clientId, clientSecret, hasAudience, audience } = answers; return { auth: { providers: { gitlab: { development: { clientId, clientSecret, ...hasAudience && { audience } } } } } }; }; const gitlab = async () => { tasks.Task.log(` To add GitLab authentication, you must create an Application from the GitLab Settings: ${chalk__default["default"].blue( "https://gitlab.com/-/profile/applications" )} The Redirect URI should point to your Backstage backend auth handler. Settings for local development: ${chalk__default["default"].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__default["default"].blue( "https://backstage.io/docs/auth/gitlab/provider" )} `); const answers = await inquirer__default["default"].prompt([ { type: "input", name: "clientId", message: "What is your Application Id?", validate: (input) => input.length ? true : false }, { type: "input", name: "clientSecret", message: "What is your Application Secret?", validate: (input) => 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) => Boolean(new URL(input)) } ]); const config = getConfig$1(answers); tasks.Task.log("Setting up GitLab Authentication for you..."); await tasks.Task.forItem( "Updating", APP_CONFIG_FILE, async () => await updateConfigFile(APP_CONFIG_FILE, config) ); const patches = await fs__namespace.readdir(PATCH_FOLDER); for (const patchFile of patches.filter((p) => p.includes("gitlab"))) { await tasks.Task.forItem("Patching", patchFile, async () => { await patch(patchFile); }); } return answers; }; async function auth() { const answers = await inquirer__default["default"].prompt([ { type: "list", name: "provider", message: "Please select an authentication provider:", choices: ["GitHub", "GitLab"] } ]); const { provider } = answers; let providerAnswers; switch (provider) { case "GitHub": { providerAnswers = await github$1(); break; } case "GitLab": { providerAnswers = await gitlab(); break; } default: throw new Error(`Provider ${provider} not implemented yet.`); } tasks.Task.log(); tasks.Task.log(`Done setting up ${provider} Authentication!`); tasks.Task.log(); return { provider, answers: providerAnswers }; } const getConfig = ({ hasEnterprise, apiBaseUrl, host, token }) => ({ integrations: { github: [ { host, token, ...hasEnterprise && { apiBaseUrl } } ] } }); const github = async (providerAnswers) => { var _a, _b, _c; const answers = await inquirer__default["default"].prompt([ { type: "confirm", name: "hasEnterprise", message: "Are you using GitHub Enterprise?", when: () => typeof providerAnswers === "undefined" }, { type: "input", name: "enterpriseInstanceUrl", message: "What is your URL for GitHub Enterprise?", when: ({ hasEnterprise }) => hasEnterprise, validate: (input) => Boolean(new URL(input)) }, { type: "input", name: "apiBaseUrl", message: "What is your GitHub Enterprise API path?", default: "/api/v3", when: ({ hasEnterprise }) => hasEnterprise || (providerAnswers == null ? void 0 : providerAnswers.hasEnterprise) // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up } ]); const host = new URL( (_b = (_a = providerAnswers == null ? void 0 : providerAnswers.enterpriseInstanceUrl) != null ? _a : answers == null ? void 0 : answers.enterpriseInstanceUrl) != null ? _b : "http://github.com" ); tasks.Task.log(` To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk__default["default"].blue( `${host.origin}/settings/tokens/new` )} Select the following scopes: Reading software components:${chalk__default["default"].cyan(` - "repo"`)} Reading organization data:${chalk__default["default"].cyan(` - "read:org" - "read:user" - "user:email"`)} Publishing software templates:${chalk__default["default"].cyan(` - "repo" - "workflow" (if templates include GitHub workflows) `)} You can find the full documentation page here: ${chalk__default["default"].blue( "https://backstage.io/docs/integrations/github/locations" )} `); const { token } = await inquirer__default["default"].prompt([ { type: "input", name: "token", message: "Please insert your personal access token to setup the GitHub Integration" // TODO(tudi2d): validate } ]); const config = getConfig({ hasEnterprise: (_c = providerAnswers == null ? void 0 : providerAnswers.hasEnterprise) != null ? _c : answers.hasEnterprise, apiBaseUrl: host.origin + answers.apiBaseUrl, host: host.hostname, token }); tasks.Task.log("Setting up Software Templates using GitHub integration for you..."); await tasks.Task.forItem( "Updating", APP_CONFIG_FILE, async () => await updateConfigFile(APP_CONFIG_FILE, config) ); }; const Integrations = ["GitHub" /* GITHUB */]; async function integrations(providerInfo) { const answers = await inquirer__default["default"].prompt([ { type: "confirm", name: "shouldUsePreviousProvider", message: `Do you want to keep using ${providerInfo == null ? void 0 : providerInfo.provider} as your provider when setting up Software Templates?`, when: () => (providerInfo == null ? void 0 : providerInfo.provider) && Object.values(Integrations).includes( providerInfo.provider ) }, { // 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; } switch (answers.integration) { case "GitHub" /* GITHUB */: { const providerAnswers = (providerInfo == null ? void 0 : providerInfo.provider) === "GitHub" ? providerInfo.answers : void 0; await github(providerAnswers); break; } } tasks.Task.log(); tasks.Task.log(`Done setting up ${answers.integration} Integration!`); tasks.Task.log(); } async function command() { const answers = await inquirer__default["default"].prompt([ { 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 } ]); const { shouldSetupAuth, shouldSetupScaffolder } = answers; let providerInfo; if (shouldSetupAuth) { providerInfo = await auth(); } if (shouldSetupScaffolder) { await integrations(providerInfo); } if (!shouldSetupAuth && !shouldSetupScaffolder) { tasks.Task.log( chalk__default["default"].yellow( "If you change your mind, feel free to re-run this command." ) ); return; } tasks.Task.log(); tasks.Task.log( `You can now start your app with ${chalk__default["default"].inverse( chalk__default["default"].italic("yarn dev") )}` ); tasks.Task.log(); } exports.command = command; //# sourceMappingURL=index-9b9f70f2.cjs.js.map