pulumi/sdk/nodejs/automation/stack.ts

1659 lines
48 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as fs from "fs";
import * as os from "os";
import * as pathlib from "path";
import * as readline from "readline";
import * as upath from "upath";
import * as grpc from "@grpc/grpc-js";
import TailFile from "@logdna/tail-file";
import * as log from "../log";
import { CommandResult } from "./cmd";
import { ConfigMap, ConfigValue } from "./config";
import { StackNotFoundError } from "./errors";
import { EngineEvent, SummaryEvent } from "./events";
import { LocalWorkspace } from "./localWorkspace";
import { LanguageServer, maxRPCMessageSize } from "./server";
import { TagMap } from "./tag";
import { Deployment, PulumiFn, Workspace } from "./workspace";
import * as langrpc from "../proto/language_grpc_pb";
/**
* {@link Stack} is an isolated, independently configurable instance of a Pulumi
* program. {@link Stack} exposes methods for the full Pulumi lifecycle
* (up/preview/refresh/destroy), as well as managing configuration. Multiple
* {@link Stacks} are commonly used to denote different phases of development
* (such as development, staging and production) or feature branches (such as
* feature-x-dev, jane-feature-x-dev).
*
* @alpha
*/
export class Stack {
/**
* The name identifying the stack.
*/
readonly name: string;
/**
* The {@link Workspace} the stack was created from.
*/
readonly workspace: Workspace;
private ready: Promise<any>;
/**
* Creates a new stack using the given workspace, and stack name.
* It fails if a stack with that name already exists
*
* @param name
* The name identifying the Stack.
* @param workspace
* The Workspace the Stack was created from.
*/
static async create(name: string, workspace: Workspace): Promise<Stack> {
const stack = new Stack(name, workspace, "create");
await stack.ready;
return stack;
}
/**
* Selects stack using the given workspace and stack name. It returns an
* error if the given stack does not exist.
*
* @param name
* The name identifying the Stack.
* @param workspace
* The {@link Workspace} the stack will be created from.
*/
static async select(name: string, workspace: Workspace): Promise<Stack> {
const stack = new Stack(name, workspace, "select");
await stack.ready;
return stack;
}
/**
* Creates a new stack using the given workspace and stack name if the stack
* does not already exist, or falls back to selecting the existing stack. If
* the stack does not exist, it will be created and selected.
*
* @param name
* The name identifying the Stack.
* @param workspace
* The {@link Workspace} the stack will be created from.
*/
static async createOrSelect(name: string, workspace: Workspace): Promise<Stack> {
const stack = new Stack(name, workspace, "createOrSelect");
await stack.ready;
return stack;
}
private constructor(name: string, workspace: Workspace, mode: StackInitMode) {
this.name = name;
this.workspace = workspace;
switch (mode) {
case "create":
this.ready = workspace.createStack(name);
return this;
case "select":
this.ready = workspace.selectStack(name);
return this;
case "createOrSelect":
this.ready = workspace.selectStack(name).catch((err) => {
if (err instanceof StackNotFoundError) {
return workspace.createStack(name);
}
throw err;
});
return this;
default:
throw new Error(`unexpected Stack creation mode: ${mode}`);
}
}
private async readLines(logPath: string, callback: (event: EngineEvent) => void): Promise<ReadlineResult> {
const eventLogTail = new TailFile(logPath, { startPos: 0, pollFileIntervalMs: 200 }).on("tail_error", (err) => {
throw err;
});
await eventLogTail.start();
const lineSplitter = readline.createInterface({ input: eventLogTail });
lineSplitter.on("line", (line) => {
let event: EngineEvent;
try {
event = JSON.parse(line);
callback(event);
} catch (e) {
log.warn(`Failed to parse engine event
If you're seeing this warning, please comment on https://github.com/pulumi/pulumi/issues/6768 with the event and any
details about your environment.
Event: ${line}\n${e.toString()}`);
}
});
return {
tail: eventLogTail,
rl: lineSplitter,
};
}
/**
* Creates or updates the resources in a stack by executing the program in
* the {@link Workspace.}
*
* @param opts
* Options to customize the behavior of the update.
*
* @see https://www.pulumi.com/docs/cli/commands/pulumi_up/
*/
async up(opts?: UpOptions): Promise<UpResult> {
const args = ["up", "--yes", "--skip-preview"];
let kind = execKind.local;
let program = this.workspace.program;
args.push(...this.remoteArgs());
if (opts) {
if (opts.program) {
program = opts.program;
}
if (opts.message) {
args.push("--message", opts.message);
}
if (opts.expectNoChanges) {
args.push("--expect-no-changes");
}
if (opts.refresh) {
args.push("--refresh");
}
if (opts.diff) {
args.push("--diff");
}
if (opts.replace) {
for (const rURN of opts.replace) {
args.push("--replace", rURN);
}
}
if (opts.target) {
for (const tURN of opts.target) {
args.push("--target", tURN);
}
}
if (opts.policyPacks) {
for (const pack of opts.policyPacks) {
args.push("--policy-pack", pack);
}
}
if (opts.policyPackConfigs) {
for (const packConfig of opts.policyPackConfigs) {
args.push("--policy-pack-config", packConfig);
}
}
if (opts.targetDependents) {
args.push("--target-dependents");
}
if (opts.parallel) {
args.push("--parallel", opts.parallel.toString());
}
if (opts.userAgent) {
args.push("--exec-agent", opts.userAgent);
}
if (opts.plan) {
args.push("--plan", opts.plan);
}
if (opts.continueOnError) {
args.push("--continue-on-error");
}
if (opts.attachDebugger) {
args.push("--attach-debugger");
}
applyGlobalOpts(opts, args);
}
let onExit = (hasError: boolean) => {
return;
};
let didError = false;
if (program) {
kind = execKind.inline;
const server = new grpc.Server({
"grpc.max_receive_message_length": maxRPCMessageSize,
});
const languageServer = new LanguageServer(program);
server.addService(langrpc.LanguageRuntimeService, languageServer);
const port: number = await new Promise<number>((resolve, reject) => {
server.bindAsync(`127.0.0.1:0`, grpc.ServerCredentials.createInsecure(), (err, p) => {
if (err) {
reject(err);
} else {
resolve(p);
}
});
});
onExit = (hasError: boolean) => {
languageServer.onPulumiExit(hasError);
server.forceShutdown();
};
args.push(`--client=127.0.0.1:${port}`);
}
args.push("--exec-kind", kind);
let logPromise: Promise<ReadlineResult> | undefined;
let logFile: string | undefined;
// Set up event log tailing
if (opts?.onEvent) {
const onEvent = opts.onEvent;
logFile = createLogFile("up");
args.push("--event-log", logFile);
logPromise = this.readLines(logFile, (event) => {
onEvent(event);
});
}
let upResult: CommandResult;
try {
upResult = await this.runPulumiCmd(args, opts?.onOutput, opts?.signal);
} catch (e) {
didError = true;
throw e;
} finally {
onExit(didError);
await cleanUp(logFile, await logPromise);
}
// TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050
const outputs = await this.outputs();
// If it's a remote workspace, explicitly set showSecrets to false to prevent attempting to
// load the project file.
const summary = await this.info(!this.isRemote && opts?.showSecrets);
return {
stdout: upResult.stdout,
stderr: upResult.stderr,
summary: summary!,
outputs: outputs!,
};
}
/**
* Performs a dry-run update to a stack, returning pending changes.
*
* @param opts Options to customize the behavior of the preview.
*
* @see https://www.pulumi.com/docs/cli/commands/pulumi_preview/
*/
async preview(opts?: PreviewOptions): Promise<PreviewResult> {
const args = ["preview"];
let kind = execKind.local;
let program = this.workspace.program;
args.push(...this.remoteArgs());
if (opts) {
if (opts.program) {
program = opts.program;
}
if (opts.message) {
args.push("--message", opts.message);
}
if (opts.expectNoChanges) {
args.push("--expect-no-changes");
}
if (opts.refresh) {
args.push("--refresh");
}
if (opts.diff) {
args.push("--diff");
}
if (opts.replace) {
for (const rURN of opts.replace) {
args.push("--replace", rURN);
}
}
if (opts.target) {
for (const tURN of opts.target) {
args.push("--target", tURN);
}
}
if (opts.policyPacks) {
for (const pack of opts.policyPacks) {
args.push("--policy-pack", pack);
}
}
if (opts.policyPackConfigs) {
for (const packConfig of opts.policyPackConfigs) {
args.push("--policy-pack-config", packConfig);
}
}
if (opts.targetDependents) {
args.push("--target-dependents");
}
if (opts.parallel) {
args.push("--parallel", opts.parallel.toString());
}
if (opts.userAgent) {
args.push("--exec-agent", opts.userAgent);
}
if (opts.plan) {
args.push("--save-plan", opts.plan);
}
if (opts.importFile) {
args.push("--import-file", opts.importFile);
}
if (opts.attachDebugger) {
args.push("--attach-debugger");
}
applyGlobalOpts(opts, args);
}
let onExit = (hasError: boolean) => {
return;
};
let didError = false;
if (program) {
kind = execKind.inline;
const server = new grpc.Server({
"grpc.max_receive_message_length": maxRPCMessageSize,
});
const languageServer = new LanguageServer(program);
server.addService(langrpc.LanguageRuntimeService, languageServer);
const port: number = await new Promise<number>((resolve, reject) => {
server.bindAsync(`127.0.0.1:0`, grpc.ServerCredentials.createInsecure(), (err, p) => {
if (err) {
reject(err);
} else {
resolve(p);
}
});
});
onExit = (hasError: boolean) => {
languageServer.onPulumiExit(hasError);
server.forceShutdown();
};
args.push(`--client=127.0.0.1:${port}`);
}
args.push("--exec-kind", kind);
// Set up event log tailing
const logFile = createLogFile("preview");
args.push("--event-log", logFile);
let summaryEvent: SummaryEvent | undefined;
const logPromise = this.readLines(logFile, (event) => {
if (event.summaryEvent) {
summaryEvent = event.summaryEvent;
}
if (opts?.onEvent) {
const onEvent = opts.onEvent;
onEvent(event);
}
});
let previewResult: CommandResult;
try {
previewResult = await this.runPulumiCmd(args, opts?.onOutput, opts?.signal);
} catch (e) {
didError = true;
throw e;
} finally {
onExit(didError);
await cleanUp(logFile, await logPromise);
}
if (!summaryEvent) {
log.warn(
"Failed to parse summary event, but preview succeeded. PreviewResult `changeSummary` will be empty.",
);
}
return {
stdout: previewResult.stdout,
stderr: previewResult.stderr,
changeSummary: summaryEvent?.resourceChanges || {},
};
}
/**
* Compares the current stacks resource state with the state known to exist
* in the actual cloud provider. Any such changes are adopted into the
* current stack.
*
* @param opts
* Options to customize the behavior of the refresh.
*/
async refresh(opts?: RefreshOptions): Promise<RefreshResult> {
const args = ["refresh", "--yes", "--skip-preview"];
args.push(...this.remoteArgs());
if (opts) {
if (opts.message) {
args.push("--message", opts.message);
}
if (opts.expectNoChanges) {
args.push("--expect-no-changes");
}
if (opts.target) {
for (const tURN of opts.target) {
args.push("--target", tURN);
}
}
if (opts.parallel) {
args.push("--parallel", opts.parallel.toString());
}
if (opts.userAgent) {
args.push("--exec-agent", opts.userAgent);
}
applyGlobalOpts(opts, args);
}
let logPromise: Promise<ReadlineResult> | undefined;
let logFile: string | undefined;
// Set up event log tailing
if (opts?.onEvent) {
const onEvent = opts.onEvent;
logFile = createLogFile("refresh");
args.push("--event-log", logFile);
logPromise = this.readLines(logFile, (event) => {
onEvent(event);
});
}
const kind = this.workspace.program ? execKind.inline : execKind.local;
args.push("--exec-kind", kind);
let refResult: CommandResult;
try {
refResult = await this.runPulumiCmd(args, opts?.onOutput, opts?.signal);
} finally {
await cleanUp(logFile, await logPromise);
}
// If it's a remote workspace, explicitly set showSecrets to false to prevent attempting to
// load the project file.
const summary = await this.info(!this.isRemote && opts?.showSecrets);
return {
stdout: refResult.stdout,
stderr: refResult.stderr,
summary: summary!,
};
}
/**
* Deletes all resources in a stack. By default, this method will leave all
* history and configuration intact. If `opts.remove` is set, the entire
* stack and its configuration will also be deleted.
*
* @param opts
* Options to customize the behavior of the destroy.
*/
async destroy(opts?: DestroyOptions): Promise<DestroyResult> {
const args = ["destroy", "--yes", "--skip-preview"];
args.push(...this.remoteArgs());
if (opts) {
if (opts.message) {
args.push("--message", opts.message);
}
if (opts.target) {
for (const tURN of opts.target) {
args.push("--target", tURN);
}
}
if (opts.targetDependents) {
args.push("--target-dependents");
}
if (opts.excludeProtected) {
args.push("--exclude-protected");
}
if (opts.continueOnError) {
args.push("--continue-on-error");
}
if (opts.parallel) {
args.push("--parallel", opts.parallel.toString());
}
if (opts.userAgent) {
args.push("--exec-agent", opts.userAgent);
}
if (opts.refresh) {
args.push("--refresh");
}
applyGlobalOpts(opts, args);
}
let logPromise: Promise<ReadlineResult> | undefined;
let logFile: string | undefined;
// Set up event log tailing
if (opts?.onEvent) {
const onEvent = opts.onEvent;
logFile = createLogFile("destroy");
args.push("--event-log", logFile);
logPromise = this.readLines(logFile, (event) => {
onEvent(event);
});
}
const kind = this.workspace.program ? execKind.inline : execKind.local;
args.push("--exec-kind", kind);
let desResult: CommandResult;
try {
desResult = await this.runPulumiCmd(args, opts?.onOutput, opts?.signal);
} finally {
await cleanUp(logFile, await logPromise);
}
// If it's a remote workspace, explicitly set showSecrets to false to prevent attempting to
// load the project file.
const summary = await this.info(!this.isRemote && opts?.showSecrets);
// If `opts.remove` was set, remove the stack now. We take this approach
// rather than passing `--remove` to `pulumi destroy` because the latter
// would make it impossible for us to retrieve a summary of the
// operation above for returning to the caller.
if (opts?.remove) {
await this.workspace.removeStack(this.name);
}
return {
stdout: desResult.stdout,
stderr: desResult.stderr,
summary: summary!,
};
}
/**
* Import resources into the stack
*
* @param options Options to specify resources and customize the behavior of the import.
*/
async import(options: ImportOptions): Promise<ImportResult> {
const args = ["import", "--yes", "--skip-preview"];
if (options.message) {
args.push("--message", options.message);
}
applyGlobalOpts(options, args);
let importResult: CommandResult;
// for import operations, generate a temporary directory to store the following:
// - the import file when the user specifies resources to import
// - the output file for the generated code
// we use the import file as input to the import command
// we the output file to read the generated code and return it to the user
let tempDir: string = "";
try {
tempDir = await fs.promises.mkdtemp(pathlib.join(os.tmpdir(), "pulumi-import-"));
const tempGeneratedCodeFile = pathlib.join(tempDir, "generated-code.txt");
if (options.resources) {
// user has specified resources to import, write them to a temp import file
const tempImportFile = pathlib.join(tempDir, "import.json");
await fs.promises.writeFile(
tempImportFile,
JSON.stringify({
nameTable: options.nameTable,
resources: options.resources,
}),
);
args.push("--file", tempImportFile);
}
if (options.generateCode === false) {
// --generate-code is set to true by default
// only use --generate-code=false if the user explicitly sets it to false
args.push("--generate-code=false");
} else {
args.push("--out", tempGeneratedCodeFile);
}
if (options.protect === false) {
// --protect is set to true by default
// only use --protect=false if the user explicitly sets it to false
args.push(`--protect=false`);
}
if (options.converter) {
// if the user specifies a converter, pass it to `--from <converter>` argument of import
args.push("--from", options.converter);
if (options.converterArgs) {
// pass any additional arguments to the converter
// for example using {
// converter: "terraform"
// converterArgs: "./tfstate.json"
// }
// would be equivalent to `pulumi import --from terraform ./tfstate.json`
args.push("--");
args.push(...options.converterArgs);
}
}
importResult = await this.runPulumiCmd(args, options.onOutput);
const summary = await this.info(!this.isRemote && options.showSecrets);
let generatedCode = "";
if (options.generateCode !== false) {
generatedCode = await fs.promises.readFile(tempGeneratedCodeFile, "utf8");
}
return {
stdout: importResult.stdout,
stderr: importResult.stderr,
generatedCode: generatedCode,
summary: summary!,
};
} finally {
if (tempDir !== "") {
// clean up temp directory we used for the import file
await fs.promises.rm(tempDir, { recursive: true });
}
}
}
/**
* Adds environments to the end of a stack's import list. Imported
* environments are merged in order per the ESC merge rules. The list of
* environments behaves as if it were the import list in an anonymous
* environment.
*
* @param environments
* The names of the environments to add to the stack's configuration
*/
async addEnvironments(...environments: string[]): Promise<void> {
await this.workspace.addEnvironments(this.name, ...environments);
}
/**
* Returns the list of environments currently in the stack's import list.
*/
async listEnvironments(): Promise<string[]> {
return this.workspace.listEnvironments(this.name);
}
/**
* Removes an environment from a stack's import list.
*
* @param environment
* The name of the environment to remove from the stack's configuration
*/
async removeEnvironment(environment: string): Promise<void> {
await this.workspace.removeEnvironment(this.name, environment);
}
/**
* Returns the config value associated with the specified key.
*
* @param key
* The key to use for the config lookup
* @param path
* The key contains a path to a property in a map or list to get
*/
async getConfig(key: string, path?: boolean): Promise<ConfigValue> {
return this.workspace.getConfig(this.name, key, path);
}
/**
* Returns the full config map associated with the stack in the workspace.
*/
async getAllConfig(): Promise<ConfigMap> {
return this.workspace.getAllConfig(this.name);
}
/**
* Sets a config key-value pair on the stack in the associated Workspace.
*
* @param key
* The key to set.
* @param value
* The config value to set.
* @param path
* The key contains a path to a property in a map or list to set.
*/
async setConfig(key: string, value: ConfigValue, path?: boolean): Promise<void> {
return this.workspace.setConfig(this.name, key, value, path);
}
/**
* Sets all specified config values on the stack in the associated
* workspace.
*
* @param config
* The map of config key-value pairs to set.
* @param path
* The keys contain a path to a property in a map or list to set.
*/
async setAllConfig(config: ConfigMap, path?: boolean): Promise<void> {
return this.workspace.setAllConfig(this.name, config, path);
}
/**
* Removes the specified config key from the stack in the associated workspace.
*
* @param key
* The config key to remove.
* @param path
* The key contains a path to a property in a map or list to remove.
*/
async removeConfig(key: string, path?: boolean): Promise<void> {
return this.workspace.removeConfig(this.name, key, path);
}
/**
* Removes the specified config keys from the stack in the associated workspace.
*
* @param keys
* The config keys to remove.
* @param path
* The keys contain a path to a property in a map or list to remove.
*/
async removeAllConfig(keys: string[], path?: boolean): Promise<void> {
return this.workspace.removeAllConfig(this.name, keys, path);
}
/**
* Gets and sets the config map used with the last update.
*/
async refreshConfig(): Promise<ConfigMap> {
return this.workspace.refreshConfig(this.name);
}
/**
* Returns the tag value associated with specified key.
*
* @param key The key to use for the tag lookup.
*/
async getTag(key: string): Promise<string> {
return this.workspace.getTag(this.name, key);
}
/**
* Sets a tag key-value pair on the stack in the associated workspace.
*
* @param key
* The tag key to set.
* @param value
* The tag value to set.
*/
async setTag(key: string, value: string): Promise<void> {
await this.workspace.setTag(this.name, key, value);
}
/**
* Removes the specified tag key-value pair from the stack in the associated
* workspace.
*
* @param key The tag key to remove.
*/
async removeTag(key: string): Promise<void> {
await this.workspace.removeTag(this.name, key);
}
/**
* Returns the full tag map associated with the stack in the workspace.
*/
async listTags(): Promise<TagMap> {
return this.workspace.listTags(this.name);
}
/**
* Gets the current set of stack outputs from the last {@link Stack.up}.
*/
async outputs(): Promise<OutputMap> {
return this.workspace.stackOutputs(this.name);
}
/**
* Returns a list summarizing all previous and current results from Stack
* lifecycle operations (up/preview/refresh/destroy).
*/
async history(pageSize?: number, page?: number, showSecrets?: boolean): Promise<UpdateSummary[]> {
const args = ["stack", "history", "--json"];
if (showSecrets ?? true) {
args.push("--show-secrets");
}
if (pageSize) {
if (!page || page < 1) {
page = 1;
}
args.push("--page-size", Math.floor(pageSize).toString(), "--page", Math.floor(page).toString());
}
const result = await this.runPulumiCmd(args);
return JSON.parse(result.stdout, (key, value) => {
if (key === "startTime" || key === "endTime") {
return new Date(value);
}
return value;
});
}
async info(showSecrets?: boolean): Promise<UpdateSummary | undefined> {
const history = await this.history(1 /*pageSize*/, undefined, showSecrets);
if (!history || history.length === 0) {
return undefined;
}
return history[0];
}
/**
* Stops a stack's currently running update. It returns an error if no
* update is currently running. Note that this operation is _very
* dangerous_, and may leave the stack in an inconsistent state if a
* resource operation was pending when the update was canceled.
*/
async cancel(): Promise<void> {
await this.runPulumiCmd(["cancel", "--yes"]);
}
/**
* Exports the deployment state of the stack. This can be combined with
* {@link Stack.importStack} to edit a stack's state (such as recovery from
* failed deployments).
*/
async exportStack(): Promise<Deployment> {
return this.workspace.exportStack(this.name);
}
/**
* Imports the specified deployment state into a pre-existing stack. This
* can be combined with {@link Stack.exportStack} to edit a stack's state
* (such as recovery from failed deployments).
*
* @param state
* The stack state to import.
*/
async importStack(state: Deployment): Promise<void> {
return this.workspace.importStack(this.name, state);
}
private async runPulumiCmd(
args: string[],
onOutput?: (out: string) => void,
signal?: AbortSignal,
): Promise<CommandResult> {
let envs: { [key: string]: string } = {
PULUMI_DEBUG_COMMANDS: "true",
};
if (this.isRemote) {
envs["PULUMI_EXPERIMENTAL"] = "true";
}
const pulumiHome = this.workspace.pulumiHome;
if (pulumiHome) {
envs["PULUMI_HOME"] = pulumiHome;
}
envs = { ...envs, ...this.workspace.envVars };
const additionalArgs = await this.workspace.serializeArgsForOp(this.name);
args = [...args, "--stack", this.name, ...additionalArgs];
const result = await this.workspace.pulumiCommand.run(args, this.workspace.workDir, envs, onOutput, signal);
await this.workspace.postCommandCallback(this.name);
return result;
}
private get isRemote(): boolean {
const ws = this.workspace;
return ws instanceof LocalWorkspace ? ws.isRemote : false;
}
private remoteArgs(): string[] {
const ws = this.workspace;
return ws instanceof LocalWorkspace ? ws.remoteArgs() : [];
}
}
interface ReadlineResult {
tail: TailFile;
rl: readline.Interface;
}
function applyGlobalOpts(opts: GlobalOpts, args: string[]) {
if (opts.color) {
args.push("--color", opts.color);
}
if (opts.logFlow) {
args.push("--logflow");
}
if (opts.logVerbosity) {
args.push("--verbose", opts.logVerbosity.toString());
}
if (opts.logToStdErr) {
args.push("--logtostderr");
}
if (opts.tracing) {
args.push("--tracing", opts.tracing);
}
if (opts.debug) {
args.push("--debug");
}
if (opts.suppressOutputs) {
args.push("--suppress-outputs");
}
if (opts.suppressProgress) {
args.push("--suppress-progress");
}
}
/**
* Returns a stack name formatted with the greatest possible specificity:
* `org/project/stack` or `user/project/stack` Using this format avoids
* ambiguity in stack identity guards creating or selecting the wrong stack.
*
* Note: legacy DIY backends (local file, S3, Azure Blob) do not support
* stack names in this format, and instead only use the stack name without an
* org/user or project to qualify it.
*
* See: https://github.com/pulumi/pulumi/issues/2522
*
* Non-legacy DIY backends do support the `org/project/stack` format, but `org`
* must be set to "organization".
*
* @param org
* The org (or user) that contains the Stack.
* @param project
* The project that parents the Stack.
* @param stack
* The name of the Stack.
*/
export function fullyQualifiedStackName(org: string, project: string, stack: string): string {
return `${org}/${project}/${stack}`;
}
/**
* A set of outputs, keyed by name, that might be returned by a Pulumi program
* as part of a stack operation.
*/
export type OutputMap = { [key: string]: OutputValue };
/**
* An output produced by a Pulumi program as part of a stack operation.
*/
export interface OutputValue {
/**
* The underlying output value.
*/
value: any;
/**
* True if and only if the value represents a secret.
*/
secret: boolean;
}
/**
* A summary of a stack operation.
*/
export interface UpdateSummary {
// Pre-update information.
/**
* The kind of operation to be executed/that was executed.
*/
kind: UpdateKind;
/**
* The time at which the operation started.
*/
startTime: Date;
/**
* An optional message associated with the operation.
*/
message: string;
/**
* The environment supplied to the operation.
*/
environment: { [key: string]: string };
/**
* The configuration used for the operation.
*/
config: ConfigMap;
// Post-update information.
/**
* The operation result.
*/
result: UpdateResult;
/**
* The time at which the operation completed.
*/
endTime: Date;
/**
* The version of the stack created by the operation.
*/
version: number;
/**
* A raw JSON blob detailing the deployment.
*/
Deployment?: RawJSON;
/**
* A summary of the changes yielded by the operation (e.g. 4 unchanged, 3
* created, etc.).
*/
resourceChanges?: OpMap;
}
/**
* The kind of update that was performed on the stack.
*/
export type UpdateKind = "update" | "preview" | "refresh" | "rename" | "destroy" | "import";
/**
* Represents the current status of a given update.
*/
export type UpdateResult = "not-started" | "in-progress" | "succeeded" | "failed";
/**
* The granular CRUD operation performed on a particular resource during an update.
*/
export type OpType =
| "same"
| "create"
| "update"
| "delete"
| "replace"
| "create-replacement"
| "delete-replaced"
| "read"
| "read-replacement"
| "refresh"
| "discard"
| "discard-replaced"
| "remove-pending-replace"
| "import"
| "import-replacement";
/**
* A map of operation types and their corresponding counts.
*/
export type OpMap = {
[key in OpType]?: number;
};
/**
* An unstructured JSON string used for back-compat with versioned APIs (such as Deployment).
*/
export type RawJSON = string;
/**
* The deployment output from running a Pulumi program update.
*/
export interface UpResult {
/**
* The standard output from the update.
*/
stdout: string;
/**
* The standard error output from the update.
*/
stderr: string;
/**
* The outputs from the update.
*/
outputs: OutputMap;
/**
* A summary of the update.
*/
summary: UpdateSummary;
}
/**
* Output from running a Pulumi program preview.
*/
export interface PreviewResult {
/**
* The standard output from the preview.
*/
stdout: string;
/**
* The standard error output from the preview.
*/
stderr: string;
/**
* A summary of the changes yielded by the operation (e.g. 4 unchanged, 3
* created, etc.).
*/
changeSummary: OpMap;
}
/**
* Output from refreshing the resources in a given Stack.
*/
export interface RefreshResult {
/**
* The standard output from the refresh.
*/
stdout: string;
/**
* The standard error output from the refresh.
*/
stderr: string;
/**
* A summary of the refresh.
*/
summary: UpdateSummary;
}
/**
* Output from destroying all resources in a Stack.
*/
export interface DestroyResult {
/**
* The standard output from the destroy.
*/
stdout: string;
/**
* The standard error output from the destroy.
*/
stderr: string;
/**
* A summary of the destroy.
*/
summary: UpdateSummary;
}
/**
* The output from performing an import operation.
*/
export interface ImportResult {
stdout: string;
stderr: string;
generatedCode: string;
summary: UpdateSummary;
}
export interface GlobalOpts {
/**
* Colorize output.
*/
color?: "always" | "never" | "raw" | "auto";
/**
* Flow log settings to child processes (like plugins)
*/
logFlow?: boolean;
/**
* Enable verbose logging (e.g., v=3); anything >3 is very verbose.
*/
logVerbosity?: number;
/**
* Log to stderr instead of to files.
*/
logToStdErr?: boolean;
/**
* Emit tracing to the specified endpoint. Use the `file:` scheme to write tracing data to a local files.
*/
tracing?: string;
/**
* Print detailed debugging output during resource operations.
* */
debug?: boolean;
/**
* Suppress display of stack outputs (in case they contain sensitive values).
*/
suppressOutputs?: boolean;
/**
* Suppress display of periodic progress dots.
*/
suppressProgress?: boolean;
/**
* Save any creates seen during the preview into an import file to use with `pulumi import`.
*/
importFile?: string;
}
/**
* Options controlling the behavior of a Stack.up() operation.
*/
export interface UpOptions extends GlobalOpts {
/**
* Allow P resource operations to run in parallel at once (1 for no parallelism).
*/
parallel?: number;
/**
* Optional message to associate with the operation.
*/
message?: string;
/**
* Return an error if any changes occur during this operation.
*/
expectNoChanges?: boolean;
/**
* Refresh the state of the stack's resources before this update.
*/
refresh?: boolean;
/**
* Display the operation as a rich diff showing the overall change.
*/
diff?: boolean;
/**
* Specify a set of resource URNs to replace.
*/
replace?: string[];
/**
* Run one or more policy packs as part of this operation.
*/
policyPacks?: string[];
/**
* A set of paths to JSON files containing configuration for the supplied `policyPacks`.
*/
policyPackConfigs?: string[];
/**
* Specify a set of resource URNs to operate on. Other resources will not be updated.
*/
target?: string[];
/**
* Operate on dependent targets discovered but not specified in `targets`.
*/
targetDependents?: boolean;
/**
* A custom user agent to use when executing the operation.
*/
userAgent?: string;
/**
* A callback to be executed when the operation produces output.
*/
onOutput?: (out: string) => void;
/**
* A callback to be executed when the operation yields an event.
*/
onEvent?: (event: EngineEvent) => void;
/**
* An inline (in-process) Pulumi program to execute the operation against.
*/
program?: PulumiFn;
/**
* Plan specifies the path to an update plan to use for the update.
*/
plan?: string;
/**
* Include secrets in the UpSummary.
*/
showSecrets?: boolean;
/**
* Continue the operation to completion even if errors occur.
*/
continueOnError?: boolean;
/**
* Run the process under a debugger, and pause until a debugger is attached.
*/
attachDebugger?: boolean;
/**
* A signal to abort an ongoing operation.
*/
signal?: AbortSignal;
}
/**
* Options controlling the behavior of a Stack.preview() operation.
*/
export interface PreviewOptions extends GlobalOpts {
/**
* Allow P resource operations to run in parallel at once (1 for no parallelism).
*/
parallel?: number;
/**
* Optional message to associate with the operation.
*/
message?: string;
/**
* Return an error if any changes occur during this operation.
*/
expectNoChanges?: boolean;
/**
* Refresh the state of the stack's resources against the cloud provider before running preview.
*/
refresh?: boolean;
/**
* Display the operation as a rich diff showing the overall change.
*/
diff?: boolean;
/**
* Specify a set of resource URNs to replace.
*/
replace?: string[];
/**
* Run one or more policy packs as part of this operation.
*/
policyPacks?: string[];
/**
* A set of paths to JSON files containing configuration for the supplied `policyPacks`.
*/
policyPackConfigs?: string[];
/**
* Specify a set of resource URNs to operate on. Other resources will not be updated.
*/
target?: string[];
/**
* Operate on dependent targets discovered but not specified in `targets`.
*/
targetDependents?: boolean;
/**
* A custom user agent to use when executing the operation.
*/
userAgent?: string;
/**
* An inline (in-process) Pulumi program to execute the operation against.
*/
program?: PulumiFn;
/**
* A callback to be executed when the operation produces output.
*/
onOutput?: (out: string) => void;
/**
* A callback to be executed when the operation yields an event.
*/
onEvent?: (event: EngineEvent) => void;
/**
* Plan specifies the path where the update plan should be saved.
*/
plan?: string;
/**
* Run the process under a debugger, and pause until a debugger is attached.
*/
attachDebugger?: boolean;
/**
* A signal to abort an ongoing operation.
*/
signal?: AbortSignal;
}
/**
* Options controlling the behavior of a Stack.refresh() operation.
*/
export interface RefreshOptions extends GlobalOpts {
/**
* Allow P resource operations to run in parallel at once (1 for no parallelism).
*/
parallel?: number;
/**
* Optional message to associate with the operation.
*/
message?: string;
/**
* Return an error if any changes occur during this operation.
*/
expectNoChanges?: boolean;
/**
* Specify a set of resource URNs to operate on. Other resources will not be updated.
*/
target?: string[];
/**
* A custom user agent to use when executing the operation.
*/
userAgent?: string;
/**
* A callback to be executed when the operation produces output.
*/
onOutput?: (out: string) => void;
/**
* A callback to be executed when the operation yields an event.
*/
onEvent?: (event: EngineEvent) => void;
/**
* Include secrets in the operation summary.
*/
showSecrets?: boolean;
/**
* A signal to abort an ongoing operation.
*/
signal?: AbortSignal;
}
/**
* Options controlling the behavior of a Stack.destroy() operation.
*/
export interface DestroyOptions extends GlobalOpts {
/**
* Allow P resource operations to run in parallel at once (1 for no parallelism).
*/
parallel?: number;
/**
* Optional message to associate with the operation.
*/
message?: string;
/**
* Refresh the state of the stack's resources against the cloud provider before running destroy.
*/
refresh?: boolean;
/**
* Specify a set of resource URNs to operate on. Other resources will not be updated.
*/
target?: string[];
/**
* Operate on dependent targets discovered but not specified in `targets`.
*/
targetDependents?: boolean;
/**
* A custom user agent to use when executing the operation.
*/
userAgent?: string;
/**
* A callback to be executed when the operation produces output.
*/
onOutput?: (out: string) => void;
/**
* A callback to be executed when the operation yields an event.
*/
onEvent?: (event: EngineEvent) => void;
/**
* Include secrets in the operation summary.
*/
showSecrets?: boolean;
/**
* Do not destroy protected resources.
*/
excludeProtected?: boolean;
/**
* Continue the operation to completion even if errors occur.
*/
continueOnError?: boolean;
/**
* Remove the stack and its configuration after all resources in the stack have been deleted.
*/
remove?: boolean;
/**
* A signal to abort an ongoing operation.
*/
signal?: AbortSignal;
}
export interface ImportResource {
/**
* The type of the resource to import
*/
type: string;
/**
* The name of the resource to import
*/
name: string;
/**
* The ID of the resource to import. The format of the ID is specific to the resource type.
*/
id?: string;
parent?: string;
provider?: string;
version?: string;
pluginDownloadUrl?: string;
logicalName?: string;
properties?: string[];
component?: boolean;
remote?: boolean;
}
/**
* Options controlling the behavior of a Stack.import() operation.
*/
export interface ImportOptions extends GlobalOpts {
/**
* The resource definitions to import into the stack
*/
resources?: ImportResource[];
/**
* The name table maps language names to parent and provider URNs. These names are
* used in the generated definitions, and should match the corresponding declarations
* in the source program. This table is required if any parents or providers are
* specified by the resources to import.
*/
nameTable?: { [key: string]: string };
/**
* Allow resources to be imported with protection from deletion enabled. Set to true by default.
*/
protect?: boolean;
/**
* Generate resource declaration code for the imported resources. Set to true by default.
*/
generateCode?: boolean;
/**
* Specify the name of a converter to import resources from.
*/
converter?: string;
/**
* Additional arguments to pass to the converter, if the user specified one.
*/
converterArgs?: string[];
/**
* Optional message to associate with the import operation
*/
message?: string;
/**
* Include secrets in the import result summary
*/
showSecrets?: boolean;
onOutput?: (out: string) => void;
}
const execKind = {
local: "auto.local",
inline: "auto.inline",
};
type StackInitMode = "create" | "select" | "createOrSelect";
const createLogFile = (command: string) => {
const logDir = fs.mkdtempSync(upath.joinSafe(os.tmpdir(), `automation-logs-${command}-`));
const logFile = upath.joinSafe(logDir, "eventlog.txt");
// just open/close the file to make sure it exists when we start polling.
fs.closeSync(fs.openSync(logFile, "w"));
return logFile;
};
const cleanUp = async (logFile?: string, rl?: ReadlineResult) => {
if (rl) {
// stop tailing
await rl.tail.quit();
// close the readline interface
rl.rl.close();
}
if (logFile) {
// remove the logfile
if (fs.rm) {
// remove with Node JS 15.X+
fs.rm(pathlib.dirname(logFile), { recursive: true }, () => {
return;
});
} else {
// remove with Node JS 14.X
fs.rmdir(pathlib.dirname(logFile), { recursive: true }, () => {
return;
});
}
}
};