From c212b57a87bd0d038f6c4d262580329acfdefacd Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 1 Sep 2019 20:28:35 +0200 Subject: [PATCH] feat: select or create new resource group THIS COMMIT INTRODUCES A LOT OF REFACTORING --- src/commands/init.ts | 12 ++- src/commands/login.ts | 22 +++-- src/commands/push.ts | 12 +++ src/commands/resource-group-create.ts | 22 +++++ src/commands/resource-group-selection.ts | 30 ++++++ src/features/hosting/command.ts | 16 ++-- src/features/hosting/index.ts | 1 + src/features/storage/commands.ts | 7 ++ src/features/storage/index.ts | 12 --- src/index.ts | 32 ++++--- src/lib/prompt.ts | 112 +++++++++++++++++++---- src/lib/utils.ts | 50 +++++++--- 12 files changed, 252 insertions(+), 76 deletions(-) create mode 100644 src/commands/push.ts create mode 100644 src/commands/resource-group-create.ts create mode 100644 src/commands/resource-group-selection.ts create mode 100644 src/features/storage/commands.ts diff --git a/src/commands/init.ts b/src/commands/init.ts index 59a89a9..0b03a8d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,4 +1,4 @@ -import { isProjectFileExists, saveProjectConfigToDisk } from "../lib/utils"; +import { isProjectFileExists, saveWorkspace, Config } from "../lib/utils"; import { askIfOverrideProjectFile, askForProjectDetails, @@ -15,21 +15,27 @@ module.exports = async function() { } const project = await askForProjectDetails(); + + + await (require(`./login`)()); + await (require(`./resource-group-selection`)()); + const { features } = await askForFeatures(); const featuresConfiguration: any = {}; for await (let feature of features) { console.log(`Configuring ${chalk.green(feature)}:`); try { - const featureImplementation = require(`./lib/features/${feature}/index`); + const featureImplementation = require(`../features/${feature}/index`); const config = await featureImplementation(); featuresConfiguration[feature] = config; + Config.get(feature, config); } catch (error) { console.error(error.toString()); } } - saveProjectConfigToDisk({ + saveWorkspace({ project, ...featuresConfiguration }); diff --git a/src/commands/login.ts b/src/commands/login.ts index 513ddb7..ee38d8e 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -1,23 +1,25 @@ -import { az, saveProjectConfigToDisk } from "../lib/utils"; +import { az, saveWorkspace, Config } from "../lib/utils"; import { chooseSubscription } from "../lib/prompt"; module.exports = async function() { - // console.log(chalk.green(`Fetching subscriptions...`)); - - // @todo save these subscriptions globally. - let subscriptions: string = await az( + let subscriptionsList = await az( `login --query '[].{name:name, state:state, id:id}'`, `Loading your subscriptions...` ); - if (subscriptions.length) { - const subscriptionsList = JSON.parse(subscriptions) as AzureSubscription[]; - let selectedSubscription = (await chooseSubscription(subscriptionsList)) + Config.set("subscriptions", subscriptionsList); + + if (subscriptionsList.length) { + let selectedSubscriptionId = (await chooseSubscription(subscriptionsList)) .subscription as string; const { id, name } = subscriptionsList.find( - (sub: AzureSubscription) => sub.name === selectedSubscription + (subscription: AzureSubscription) => + subscription.id === selectedSubscriptionId ) as AzureSubscription; - saveProjectConfigToDisk({ + + Config.set("subscription", { id, name }); + + saveWorkspace({ subscription: { id, name diff --git a/src/commands/push.ts b/src/commands/push.ts new file mode 100644 index 0000000..f868e06 --- /dev/null +++ b/src/commands/push.ts @@ -0,0 +1,12 @@ +import { push } from "../features/hosting/command"; +import { Config } from "../lib/utils"; + +module.exports = async function() { + const subscription = Config.get("subscription") as AzureSubscription; + const storage = Config.get("storage") as AzureStorage; + + await push({ + subscriptionId: subscription.id, + storageAccountName: storage.name + }); +}; diff --git a/src/commands/resource-group-create.ts b/src/commands/resource-group-create.ts new file mode 100644 index 0000000..3b9e584 --- /dev/null +++ b/src/commands/resource-group-create.ts @@ -0,0 +1,22 @@ +import { askForResourceGroupDetails } from "../lib/prompt"; +import { az, saveWorkspace } from "../lib/utils"; + +module.exports = async function() { + let regionsList = await az( + `account list-locations --query '[].{name:name, id:id, displayName:displayName}'`, + `Loading your regions (this may take few minutes)...` + ); + + const { resource, region } = await askForResourceGroupDetails(regionsList); + + let resourceGroup = await az( + `group create -l ${region} -n ${resource} --tag cli=nitro --query '[].{name:name, id:id, location:location}'`, + `Creating your resource group...` + ); + + console.log("asdasdsdasdasd"); + + saveWorkspace({ + resourceGroup + }); +}; diff --git a/src/commands/resource-group-selection.ts b/src/commands/resource-group-selection.ts new file mode 100644 index 0000000..182e457 --- /dev/null +++ b/src/commands/resource-group-selection.ts @@ -0,0 +1,30 @@ +import { chooseResourceGroup } from "../lib/prompt"; +import { az, Config, saveWorkspace } from "../lib/utils"; + +module.exports = async function() { + let resourceGroupsList = await az( + `group list --query '[].{name:name, id:id, location:location}'`, + `Loading your resource groups...` + ); + + if (resourceGroupsList.length) { + let selectedResourceId = (await chooseResourceGroup(resourceGroupsList)).resourceGroup as string; + + if (selectedResourceId === "") { + // create a new resource group + return (await require(`./resource-group-create`))(); + } else { + const { id, name, location } = resourceGroupsList.find( + (resourceGroup: AzureResourceGroup) => resourceGroup.id === selectedResourceId + ) as AzureResourceGroup; + + saveWorkspace({ + resourceGroup: { + id, + location, + name + } + }); + } + } +}; diff --git a/src/features/hosting/command.ts b/src/features/hosting/command.ts index 37ece26..40275e3 100644 --- a/src/features/hosting/command.ts +++ b/src/features/hosting/command.ts @@ -1,13 +1,13 @@ import { az } from "../../lib/utils"; -export async function push() { +export async function push({ subscriptionId, storageAccountName }: { subscriptionId: string, storageAccountName: string }) { await az( - `storage blob service-properties update --account-name --static-website --404-document --index-document ` - ); - await az( - `storage blob upload-batch -s -d \$web --account-name ` - ); - await az( - `storage account show -n -g --query "primaryEndpoints.web"` + `storage blob service-properties update --account-name ${storageAccountName} --static-website --404-document 404.html --index-document index.html` ); + // await az( + // `storage blob upload-batch -s -d \$web --account-name ` + // ); + // await az( + // `storage account show -n -g --query "primaryEndpoints.web"` + // ); } diff --git a/src/features/hosting/index.ts b/src/features/hosting/index.ts index 57ef1c8..d5f6447 100644 --- a/src/features/hosting/index.ts +++ b/src/features/hosting/index.ts @@ -12,6 +12,7 @@ module.exports = async function(): Promise { default: "public", validate: function(value: string) { if (value && value.length) { + // TODO: copy template files if new created folder return createDirectoryIfNotExists(value); } else { return "Please enter a public folder."; diff --git a/src/features/storage/commands.ts b/src/features/storage/commands.ts new file mode 100644 index 0000000..bcf583e --- /dev/null +++ b/src/features/storage/commands.ts @@ -0,0 +1,7 @@ +import { az } from "../../lib/utils"; + +export async function create({ name }: AzureStorage) { + await az( + `storage account create -n ${name} -g MyResourceGroup -l westus --sku Standard_LRS` + ); +} diff --git a/src/features/storage/index.ts b/src/features/storage/index.ts index 4ebd4ea..f00e188 100644 --- a/src/features/storage/index.ts +++ b/src/features/storage/index.ts @@ -15,18 +15,6 @@ module.exports = async function(): Promise { return "Please enter a valid name."; } } - }, - { - type: "input", - name: "sas", - message: "Enter your storage SAS token:", - validate: function(value: string) { - if (value.length) { - return true; - } else { - return "Please enter a valid SAS token."; - } - } } ]; return inquirer.prompt(questions); diff --git a/src/index.ts b/src/index.ts index 8a7c79f..e6e0d0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ #!/usr/bin/env node +process.env.DEBUG = "*"; + import chalk from "chalk"; import clear from "clear"; import figlet from "figlet"; @@ -16,25 +18,31 @@ console.log( ); (async () => { + const runCommand = async (commandName: string) => { + try { + return (await require(`./commands/${commandName}`))(); + } catch (error) { + console.error(chalk.red(`Command "${commandName}" not supported yet.`)); + console.error(chalk.red(error)); + program.outputHelp(); + } + }; program .name("nitro") .usage("") .version(require("../package.json").version) - .option("--init", "initialise a new workspace") - .option("--login", "connect to your Azure") - .option("--push", "deploy the app to Azure") + .option("login, --login", "connect to your Azure") + .option("init, --init", "initialise a new workspace") + .option("push, --push", "deploy the app to Azure") .parse(process.argv); - if (!process.argv.slice(2).length) { - program.outputHelp(); - } + // use process.argv not program.argv + const commandName = process.argv[2]; - const commandName = program.args[0]; - - try { - (await require(`./commands/${commandName}`))(); - } catch (error) { - console.error(chalk.red(`Command ${commandName} not supported.`)); + if (!process.argv.slice(2).length || !commandName) { program.outputHelp(); + process.exit(0); } + + runCommand(commandName.replace("--", "")); })(); diff --git a/src/lib/prompt.ts b/src/lib/prompt.ts index 679b2da..cfe0ca3 100644 --- a/src/lib/prompt.ts +++ b/src/lib/prompt.ts @@ -1,23 +1,53 @@ import inquirer, { Answers, QuestionCollection } from "inquirer"; import { getCurrentDirectoryBase } from "./utils"; - -export function chooseSubscription(subscriptionsList: any[]): Promise { +const uuid = require("uuid"); +export function chooseSubscription(subscriptionsList: AzureSubscription[]): Promise { const questions: QuestionCollection = [ { type: "list", name: "subscription", message: "Choose your subscription:", - choices: subscriptionsList.map((sub: AzureSubscription) => { + choices: subscriptionsList.map((subscription: AzureSubscription) => { return { - name: `${sub.name}`, - disabled: sub.state !== "Enabled" + name: `${subscription.name}`, + disabled: subscription.state !== "Enabled", + value: subscription.id }; }), validate: function(value: string) { if (value.length) { return true; } else { - return "Please enter a name for the project."; + return "Please choose a subscription."; + } + } + } + ]; + return inquirer.prompt(questions); +} + +export function chooseResourceGroup(resourceGroups: AzureResourceGroup[]): Promise { + const extraChoice: AzureResourceGroup = { + id: "", + location: "", + name: "" + }; + const questions: QuestionCollection = [ + { + type: "list", + name: "resourceGroup", + message: "Choose your resource group:", + choices: [...[extraChoice], ...resourceGroups].map((resourceGroup: AzureResourceGroup) => { + return { + name: `${resourceGroup.name}`, + value: resourceGroup.id + }; + }), + validate: function(value: string) { + if (value.length) { + return true; + } else { + return "Please enter a resource group."; } } } @@ -25,6 +55,24 @@ export function chooseSubscription(subscriptionsList: any[]): Promise { return inquirer.prompt(questions); } +export function chooseAccountStorageName(): Promise { + const questions: QuestionCollection = [ + { + type: "input", + name: "name", + message: "Enter your storage account name:", + validate: function(value: string) { + if (value.length) { + return true; + } else { + return "Please enter a valid name."; + } + } + } + ]; + return inquirer.prompt(questions); +} + export function askForFeatures(): Promise { const questions: QuestionCollection = [ { @@ -34,27 +82,22 @@ export function askForFeatures(): Promise { choices: [ { name: "storage", - checked: true, - required: true + checked: true }, { name: "hosting" }, { - name: "functions (coming soon)", - disabled: true + name: "functions (coming soon)" }, { - name: "database (coming soon)", - disabled: true + name: "database (coming soon)" }, { - name: "cdn (coming soon)", - disabled: true + name: "cdn (coming soon)" }, { - name: "auth (coming soon)", - disabled: true + name: "auth (coming soon)" } ], validate: function(value: string) { @@ -69,6 +112,43 @@ export function askForFeatures(): Promise { return inquirer.prompt(questions); } +export function askForResourceGroupDetails(regions: AzureRegion[]): Promise { + const questions: QuestionCollection = [ + { + type: "input", + name: "resource", + message: "Enter a name for the resource group:", + default: `nitro-${uuid()}`, + validate: function(value: string) { + if (value.length) { + return true; + } else { + return "Please enter a name for the resource group."; + } + } + }, + { + type: "list", + name: "region", + message: "Choose a region:", + choices: regions.map((region: AzureRegion) => { + return { + name: `${region.name} (${region.displayName})`, + value: region.name, + short: region.displayName + }; + }), + validate: function(value: string) { + if (value.length) { + return true; + } else { + return "Please choose a region."; + } + } + } + ]; + return inquirer.prompt(questions); +} export function askForProjectDetails(): Promise { const questions: QuestionCollection = [ { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6692ca9..138cdac 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,32 +2,52 @@ import fs from "fs"; import path from "path"; const shell = require("shelljs"); const ora = require("ora"); +const Configstore = require("configstore"); +const packageJson = require("../../package.json"); +const debug = require("debug")("nitro"); +export const Config = new Configstore(packageJson.name, { + version: packageJson.version +}); export const WORKSPACE_FILENAME = "nitro.json"; -export async function runCmd(command: string, loadingMessage?: string): Promise { +export async function runCmd(command: string, loadingMessage?: string, options?: CommandOptions): Promise { let spinner: typeof ora = null; - if (loadingMessage) { + if (loadingMessage && debug.enabled === false) { spinner = ora(loadingMessage).start(); } - return new Promise((resolve, _reject) => { - const { stdout } = shell.exec(`${command} --output json`, { - silent: true, - async: true - }); - stdout.on("data", (data: string) => { - resolve(data); + return new Promise((resolve, reject) => { + command = `${command} --output json ` + (debug.enabled && "--verbose"); - if (spinner) { - spinner.stop(); + debug(command); + + shell.exec( + command, + { + ...options + }, + (code: number, stdout: string, stderr: string) => { + if (stderr.length) { + debug("stderr", stderr); + } + if (stdout.length) { + debug("stdout", stdout); + resolve(stdout); + } + try { + spinner.stop(); + } catch (error) {} } - }); + ); }); } -export async function az(command: string, loadingMessage?: string) { - return await runCmd(`az ${command}`, loadingMessage); +export async function az(command: string, loadingMessage?: string) { + const output: string = await runCmd(`az ${command}`, loadingMessage, { + silent: !debug.enabled + }); + return JSON.parse(output || "{}") as T; } export function getCurrentDirectoryBase() { @@ -65,7 +85,7 @@ export function readFileFromDisk(filePath: string) { return null; } -export function saveProjectConfigToDisk(config: object) { +export function saveWorkspace(config: object) { let oldConfig = {}; if (fileExists(WORKSPACE_FILENAME)) { oldConfig = JSON.parse(readFileFromDisk(WORKSPACE_FILENAME) || "{}");