@backstage/cli
Version:
CLI for developing Backstage plugins and apps
472 lines (448 loc) • 14.4 kB
JavaScript
;
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