Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/small-frogs-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@plutolang/simulator-adapter": patch
"@plutolang/pulumi-adapter": patch
"@plutolang/pluto-infra": patch
"@plutolang/base": patch
"@plutolang/cli": patch
---

Feature: refactor Pluto's output management

Refactor Pluto's output management by introducing a dedicated directory for the adapter to maintain its state. Migrate all state-related configurations, including lastArchRefFile, from the existing configuration file to this new state directory.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ yarn-error.log*
**/dist/

# pluto
**/compiled/
**/.pluto/**/state/
**/.pluto/*.test.*/

devapps/

Expand Down
188 changes: 103 additions & 85 deletions apps/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,145 +1,163 @@
import path, { resolve } from "path";
import fs from "fs";
import * as path from "path";
import * as fs from "fs-extra";
import { table, TableUserConfig } from "table";
import { confirm } from "@inquirer/prompts";
import { arch, core } from "@plutolang/base";
import { arch, config, core } from "@plutolang/base";
import logger from "../log";
import { loadAndDeduce, loadAndGenerate } from "./compile";
import { buildAdapter, loadArchRef, selectAdapterByEngine } from "./utils";
import { loadProject, dumpProject, PLUTO_PROJECT_OUTPUT_DIR, isPlutoProject } from "../utils";
import { ensureDirSync } from "fs-extra";
import {
buildAdapterByProvisionType,
loadArchRef,
loadProjectAndStack,
loadProjectRoot,
stackStateFile,
} from "./utils";
import { dumpStackState, prepareStackDirs } from "../utils";

export interface DeployOptions {
stack?: string;
deducer: string;
generator: string;
apply: boolean;
yes: boolean;
force: boolean;
}

export async function deploy(entrypoint: string, opts: DeployOptions) {
// Ensure the entrypoint exist.
if (!fs.existsSync(entrypoint)) {
throw new Error(`No such file, ${entrypoint}`);
}

const projectRoot = resolve("./");
if (!isPlutoProject(projectRoot)) {
logger.error("The current location is not located at the root of a Pluto project.");
logger.error(`The entry point file '${entrypoint}' does not exist.`);
process.exit(1);
}
const project = loadProject(projectRoot);

const stackName = opts.stack ?? project.current;
if (!stackName) {
logger.error(
"There isn't a default stack. Please use the --stack option to specify which stack you want."
try {
const projectRoot = loadProjectRoot();
const { project, stack } = loadProjectAndStack(projectRoot, opts.stack);

// Prepare the directories for the stack.
const { baseDir, closuresDir, generatedDir, stateDir } = await prepareStackDirs(
projectRoot,
stack.name
);

const { archRef, infraEntrypoint } = await buildArchRefAndInfraEntrypoint(
project,
stack,
entrypoint,
opts,
closuresDir,
generatedDir
);
process.exit(1);
}

const stack = project.getStack(stackName);
if (!stack) {
logger.error(`There is no stack named ${stackName}.`);
// Save the filepath to the arch ref and the infra entrypoint in the stack state file.
const archRefFile = path.join(baseDir, "arch.yml");
fs.writeFileSync(archRefFile, archRef.toYaml());
stack.archRefFile = archRefFile;
stack.provisionFile = infraEntrypoint;
dumpStackState(stackStateFile(stateDir), stack.state);

// Build the adapter and deploy the stack.
const adapter = await buildAdapterByProvisionType(stack.provisionType, {
project: project.name,
rootpath: project.rootpath,
stack: stack,
archRef: archRef,
entrypoint: infraEntrypoint!,
stateDir: stateDir,
});
await deployWithAdapter(adapter, stack, opts.force);

// Save the stack state after the deployment.
dumpStackState(stackStateFile(stateDir), stack.state);
} catch (e) {
if (e instanceof Error) {
logger.error(e.message);
if (process.env.DEBUG) {
logger.error(e.stack);
}
} else {
logger.error(e);
}
process.exit(1);
}
}

process.env["PLUTO_PROJECT_NAME"] = project.name;
process.env["PLUTO_STACK_NAME"] = stack.name;
/**
* @param entrypoint - The entrypoint file of the user code.
* @param closuresDir - The directory stores the closures deduced by the deducer.
* @param generatedDir - The directory stores the files generated by the generator.
*/
async function buildArchRefAndInfraEntrypoint(
project: config.Project,
stack: config.Stack,
entrypoint: string,
options: DeployOptions,
closuresDir: string,
generatedDir: string
) {
let archRef: arch.Architecture | undefined;
let infraEntrypoint: string | undefined;

const basicArgs: core.BasicArgs = {
project: project.name,
rootpath: projectRoot,
stack: stack,
rootpath: project.rootpath,
};
const stackBaseDir = path.join(projectRoot, PLUTO_PROJECT_OUTPUT_DIR, stackName);
const closureBaseDir = path.join(stackBaseDir, "closures");
const generatedDir = path.join(stackBaseDir, "generated");
ensureDirSync(generatedDir);

let archRef: arch.Architecture | undefined;
let infraEntrypoint: string | undefined;
// No deduction or generation, only application.
if (!opts.apply) {
if (!options.apply) {
// construct the arch ref from user code
logger.info("Generating reference architecture...");
const deduceResult = await loadAndDeduce(
opts.deducer,
options.deducer,
{
...basicArgs,
closureDir: closureBaseDir,
closureDir: closuresDir,
},
[entrypoint]
);
archRef = deduceResult.archRef;

const archRefFile = path.join(stackBaseDir, "arch.yml");
fs.writeFileSync(archRefFile, archRef.toYaml());
stack.archRefFile = archRefFile;

const confirmed = await confirmArch(archRef, opts.yes);
const confirmed = await confirmArch(archRef, options.yes);
if (!confirmed) {
logger.info("You can modify your code and try again.");
process.exit(1);
process.exit(0);
}

// generate the IR code based on the arch ref
logger.info("Generating the IaC Code and computing modules...");
const generateResult = await loadAndGenerate(opts.generator, basicArgs, archRef, generatedDir);
const generateResult = await loadAndGenerate(
options.generator,
basicArgs,
archRef,
generatedDir
);
infraEntrypoint = path.resolve(generatedDir, generateResult.entrypoint!);
stack.provisionFile = infraEntrypoint;

dumpProject(project);
} else {
if (!stack.archRefFile || !stack.provisionFile) {
logger.error("Please avoid using the --apply option during the initial deployment.");
process.exit(1);
throw new Error("Please avoid using the --apply option during the initial deployment.");
}
archRef = loadArchRef(stack.archRefFile);
infraEntrypoint = stack.provisionFile;
}

// TODO: make the workdir same with generated dir.
const workdir = generatedDir;
// build the adapter based on the provisioning engine type
const adapterPkg = selectAdapterByEngine(stack.provisionType);
if (!adapterPkg) {
logger.error(`There is no adapter for type ${stack.provisionType}.`);
process.exit(1);
}
const adapter = await buildAdapter(adapterPkg, {
...basicArgs,
archRef: archRef,
entrypoint: infraEntrypoint!,
workdir: workdir,
});
if (stack.adapterState) {
adapter.load(stack.adapterState);
}
return { archRef, infraEntrypoint };
}

let exitCode = 0;
try {
logger.info("Applying...");
const applyResult = await adapter.deploy();
stack.setDeployed();
logger.info("Successfully applied!");

logger.info("Here are the resource outputs:");
for (const key in applyResult.outputs) {
logger.info(`${key}:`, applyResult.outputs[key]);
}
} catch (e) {
if (e instanceof Error) {
logger.error(e.message);
} else {
logger.error(e);
}
exitCode = 1;
} finally {
stack.adapterState = adapter.dump();
dumpProject(project);
async function deployWithAdapter(
adapter: core.Adapter,
stack: config.Stack,
force: boolean = false
) {
logger.info("Applying...");
const applyResult = await adapter.deploy({ force });
stack.setDeployed();
logger.info("Successfully applied!");

logger.info("Here are the resource outputs:");
for (const key in applyResult.outputs) {
logger.info(`${key}:`, applyResult.outputs[key]);
}
process.exit(exitCode);
}

async function confirmArch(archRef: arch.Architecture, confirmed: boolean): Promise<boolean> {
Expand Down
91 changes: 29 additions & 62 deletions apps/cli/src/commands/destory.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,50 @@
import path from "path";
import logger from "../log";
import { buildAdapter, loadArchRef, selectAdapterByEngine } from "./utils";
import { PLUTO_PROJECT_OUTPUT_DIR, dumpProject, isPlutoProject, loadProject } from "../utils";
import {
buildAdapterByProvisionType,
loadArchRef,
loadProjectAndStack,
loadProjectRoot,
stackStateFile,
} from "./utils";
import { dumpStackState, getStackBasicDirs } from "../utils";

export interface DestoryOptions {
stack?: string;
clean?: boolean;
}

export async function destroy(opts: DestoryOptions) {
const projectRoot = path.resolve("./");
if (!isPlutoProject(projectRoot)) {
logger.error("The current location is not located at the root of a Pluto project.");
process.exit(1);
}
const proj = loadProject(projectRoot);

const stackName = opts.stack ?? proj.current;
if (!stackName) {
logger.error(
"There isn't a default stack. Please use the --stack option to specify which stack you want."
);
process.exit(1);
}

const stack = proj.getStack(stackName);
if (!stack) {
logger.error(`There is no stack named ${stackName}.`);
process.exit(1);
}

// If the user sets the --clean option, there is no need for us to check if the stack has been deployed.
// We will clean up all the resources that have been created, even if the stack has not been deployed.
if (!opts.clean && !stack.isDeployed()) {
logger.error("This stack hasn't been deployed yet. Please deploy it first.");
process.exit(1);
}
try {
const projectRoot = loadProjectRoot();
const { project, stack } = loadProjectAndStack(projectRoot, opts.stack);

if (!stack.archRefFile || !stack.provisionFile) {
logger.error(
"There are some configurations missing in this stack. You can try redeploying the stack and give it another go."
);
process.exit(1);
}
if (!stack.archRefFile || !stack.provisionFile) {
throw new Error(
"The stack is missing an architecture reference file and a provision file. Please execute the `pluto deploy` command again before proceeding with the destruction."
);
}

const stackBaseDir = path.join(projectRoot, PLUTO_PROJECT_OUTPUT_DIR, stackName);
const generatedDir = path.join(stackBaseDir, "generated");
const { stateDir } = getStackBasicDirs(projectRoot, stack.name);

// build the adapter based on the provisioning engine type
const adapterPkg = selectAdapterByEngine(stack.provisionType);
if (!adapterPkg) {
logger.error(`There is no adapter for type ${stack.provisionType}.`);
process.exit(1);
}
const adapter = await buildAdapter(adapterPkg, {
project: proj.name,
rootpath: projectRoot,
stack: stack,
archRef: loadArchRef(stack.archRefFile),
entrypoint: stack.provisionFile,
workdir: generatedDir,
});
if (!adapter) {
logger.error(`There is no engine of type ${stack.provisionType}.`);
process.exit(1);
}
if (stack.adapterState) {
adapter.load(stack.adapterState);
}
const adapter = await buildAdapterByProvisionType(stack.provisionType, {
project: project.name,
rootpath: project.rootpath,
stack: stack,
archRef: loadArchRef(stack.archRefFile),
entrypoint: stack.provisionFile,
stateDir: stateDir,
});

try {
logger.info("Destroying...");
await adapter.destroy();
stack.setUndeployed();
dumpProject(proj);
dumpStackState(stackStateFile(stateDir), stack.state);
logger.info("Successfully destroyed!");
} catch (e) {
if (e instanceof Error) {
logger.error(e.message);
if (process.env.DEBUG) {
logger.error(e.stack);
}
} else {
logger.error(e);
}
Expand Down
Loading