import { AllResourceGroupTypes, importResourceGroupType, ResourceGroupType, readPulumiProjectMetadata } from "@/resourceGroups"; import { Command } from "commander"; import { z } from "zod"; import fs from "fs/promises"; import path from "path"; import prompts from "prompts"; import { spinner } from "@/cli-helpers"; import { execPromise } from "@/exec"; const envPullOptionsSchema = z.object({ resourceGroup: z.string().optional(), stack: z.string().optional(), directory: z.string().optional().default(process.cwd()) }); export type EnvPullOptions = z.infer; export const envPull = new Command() .name("env-pull") .description( "Pull resource group environment variables into a local .env file" ) .option( "-p, --resourceGroupName ", "Name of the resource group to pull environment variables from" ) .option( "-s, --stack ", "Name of the stack to pull environment variables from" ) .option( "-d, --directory ", "Directory for the base infrastructure folder", process.cwd() ) .action(async (options: any) => { const cmdArgs = envPullOptionsSchema.parse({ options }); if (options.directory) { process.chdir(options.directory); } const resourceGroupName = await getResourceGroupName(cmdArgs); const resourceGroupType = await getResourceGroupType({ resourceGroupName: resourceGroupName }); const resourceGroup = await importResourceGroupType( resourceGroupType, resourceGroupName, cmdArgs.directory ); if (!resourceGroup) { throw new Error( `Could not import resource group type ${resourceGroupType}` ); } if (!resourceGroup.envPull) { throw new Error( `Resource group type ${resourceGroupType} does not support env-pull` ); } const stack = await getStackName({ resourceGroupName: resourceGroupName, stack: cmdArgs.stack }); const msg = spinner( `Pulling environment variables for ${resourceGroupName} stack ${stack}` ).start(); await resourceGroup.envPull({ resourceGroupName: resourceGroupName, stack }); msg.succeed( `Environment variables pulled for ${resourceGroupName} stack ${stack}` ); }); async function getResourceGroupName(cmdArgs: EnvPullOptions): Promise { if (cmdArgs.resourceGroup) { return cmdArgs.resourceGroup; } // get the resource group name from a prompt by reading what's in the infrastructure folder relative to ${directory} const resourceGroups = await fs.readdir("infrastructure/resourceGroups"); // use a prompt from prompts() furnction const { resourceGroupName } = await prompts({ type: "select", name: "resourceGroupName", message: "Select a resource group", choices: resourceGroups.map((resourceGroup) => ({ title: resourceGroup, value: resourceGroup })) }); return resourceGroupName; } async function getResourceGroupType({ resourceGroupName }: { resourceGroupName: string; }): Promise { const metadata = await readPulumiProjectMetadata({ resourceGroupName: resourceGroupName }); if ( !AllResourceGroupTypes.includes( metadata.resourceGroupType as ResourceGroupType ) ) { throw new Error( `Unsupported resource group type: ${metadata.resourceGroupType}` ); } return metadata.resourceGroupType as ResourceGroupType; } async function getStackName(opts: { resourceGroupName: string; stack?: string; }) { if (opts.stack) { return opts.stack; } const { stdout } = await execPromise("pulumi stack ls --json", { cwd: path.join("infrastructure", "resourceGroups", opts.resourceGroupName) }); const stacks = JSON.parse(stdout); if (stacks.length === 0) { throw new Error("No stacks found"); } const { stackName } = await prompts({ type: "select", name: "stackName", message: "Select a stack", choices: stacks.map((stack: any) => ({ title: stack.name, value: stack.name })) }); return stackName; }