Skip to content

Commit

Permalink
[auto/nodejs] Support for remote operations
Browse files Browse the repository at this point in the history
  • Loading branch information
justinvp committed Oct 28, 2022
1 parent bd87211 commit adde4aa
Show file tree
Hide file tree
Showing 8 changed files with 624 additions and 14 deletions.
@@ -0,0 +1,4 @@
changes:
- type: feat
scope: auto/nodejs
description: Support for remote operations
4 changes: 3 additions & 1 deletion sdk/nodejs/automation/index.ts
@@ -1,4 +1,4 @@
// Copyright 2016-2020, Pulumi Corporation.
// 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.
Expand All @@ -21,3 +21,5 @@ export * from "./projectSettings";
export * from "./localWorkspace";
export * from "./workspace";
export * from "./events";
export * from "./remoteStack";
export * from "./remoteWorkspace";
134 changes: 132 additions & 2 deletions sdk/nodejs/automation/localWorkspace.ts
@@ -1,4 +1,4 @@
// Copyright 2016-2021, Pulumi Corporation.
// 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.
Expand All @@ -22,6 +22,7 @@ import { CommandResult, runPulumiCmd } from "./cmd";
import { ConfigMap, ConfigValue } from "./config";
import { minimumVersion } from "./minimumVersion";
import { ProjectSettings } from "./projectSettings";
import { RemoteGitProgramArgs } from "./remoteWorkspace";
import { OutputMap, Stack } from "./stack";
import * as localState from "../runtime/state";
import { StackSettings, stackSettingsSerDeKeys } from "./stackSettings";
Expand Down Expand Up @@ -79,6 +80,27 @@ export class LocalWorkspace implements Workspace {
return this._pulumiVersion.toString();
}
private ready: Promise<any[]>;

/**
* Whether the workspace is a remote workspace.
*/
private remote?: boolean;

/**
* Remote Git source info.
*/
private remoteGitProgramArgs?: RemoteGitProgramArgs;

/**
* An optional list of arbitrary commands to run before the remote Pulumi operation is invoked.
*/
private remotePreRunCommands?: string[];

/**
* The environment variables to pass along when running remote Pulumi operations.
*/
private remoteEnvVars?: { [key: string]: string | { secret: string } };

/**
* Creates a workspace using the specified options. Used for maximal control and customization
* of the underlying environment before any stacks are created or selected.
Expand Down Expand Up @@ -223,13 +245,18 @@ export class LocalWorkspace implements Workspace {
localState.asyncLocalStorage.enterWith(store);

if (opts) {
const { workDir, pulumiHome, program, envVars, secretsProvider } = opts;
const { workDir, pulumiHome, program, envVars, secretsProvider,
remote, remoteGitProgramArgs, remotePreRunCommands, remoteEnvVars } = opts;
if (workDir) {
dir = workDir;
}
this.pulumiHome = pulumiHome;
this.program = program;
this.secretsProvider = secretsProvider;
this.remote = remote;
this.remoteGitProgramArgs = remoteGitProgramArgs;
this.remotePreRunCommands = remotePreRunCommands;
this.remoteEnvVars = { ...remoteEnvVars };
envs = { ...envVars };
}

Expand Down Expand Up @@ -363,6 +390,9 @@ export class LocalWorkspace implements Workspace {
if (this.secretsProvider) {
args.push("--secrets-provider", this.secretsProvider);
}
if (this.isRemote) {
args.push("--no-select");
}
await this.runPulumiCmd(args);
}
/**
Expand Down Expand Up @@ -611,6 +641,16 @@ export class LocalWorkspace implements Workspace {
if (version != null) {
this._pulumiVersion = version;
}

// If remote was specified, ensure the CLI supports it.
if (!optOut && this.isRemote) {
// See if `--remote` is present in `pulumi preview --help`'s output.
const previewResult = await this.runPulumiCmd(["preview", "--help"]);
const previewOutput = previewResult.stdout.trim();
if (!previewOutput.includes("--remote")) {
throw new Error("The Pulumi CLI does not support remote operations. Please upgrade.");
}
}
}
private async runPulumiCmd(
args: string[],
Expand All @@ -619,9 +659,75 @@ export class LocalWorkspace implements Workspace {
if (this.pulumiHome) {
envs["PULUMI_HOME"] = this.pulumiHome;
}
if (this.isRemote) {
envs["PULUMI_EXPERIMENTAL"] = "true";
}
envs = { ...envs, ...this.envVars };
return runPulumiCmd(args, this.workDir, envs);
}
/** @internal */
get isRemote(): boolean {
return !!this.remote;
}
/** @internal */
remoteArgs(): string[] {
const args: string[] = [];
if (!this.isRemote) {
return args;
}

args.push("--remote");
if (this.remoteGitProgramArgs) {
const { url, projectPath, branch, commitHash, auth } = this.remoteGitProgramArgs;
if (url) {
args.push(url);
}
if (projectPath) {
args.push("--remote-git-repo-dir", projectPath);
}
if (branch) {
args.push("--remote-git-branch", branch);
}
if (commitHash) {
args.push("--remote-git-commit", commitHash);
}
if (auth) {
const { personalAccessToken, sshPrivateKey, sshPrivateKeyPath, password, username } = auth;
if (personalAccessToken) {
args.push("--remote-git-auth-access-token", personalAccessToken);
}
if (sshPrivateKey) {
args.push("--remote-git-auth-ssh-private-key", sshPrivateKey);
}
if (sshPrivateKeyPath) {
args.push("--remote-git-auth-ssh-private-key-path", sshPrivateKeyPath);
}
if (password) {
args.push("--remote-git-auth-password", password);
}
if (username) {
args.push("--remote-git-username", username);
}
}
}

for (const key of Object.keys(this.remoteEnvVars ?? {})) {
const val = this.remoteEnvVars![key];
if (typeof val === "string") {
args.push("--remote-env", `${key}=${val}`);
} else if ("secret" in val) {
args.push("--remote-env-secret", `${key}=${val.secret}`);
} else {
throw new Error(`unexpected env value '${val}' for key '${key}'`);
}
}

for (const command of this.remotePreRunCommands ?? []) {
args.push("--remote-pre-run-command", command);
}

return args;
}
}

/**
Expand Down Expand Up @@ -685,6 +791,30 @@ export interface LocalWorkspaceOptions {
* A map of Stack names and corresponding settings objects.
*/
stackSettings?: { [key: string]: StackSettings };
/**
* Indicates that the workspace is a remote workspace.
*
* @internal
*/
remote?: boolean;
/**
* The remote Git source info.
*
* @internal
*/
remoteGitProgramArgs?: RemoteGitProgramArgs;
/**
* An optional list of arbitrary commands to run before a remote Pulumi operation is invoked.
*
* @internal
*/
remotePreRunCommands?: string[];
/**
* The environment variables to pass along when running remote Pulumi operations.
*
* @internal
*/
remoteEnvVars?: { [key: string]: string | { secret: string } };
}

/**
Expand Down
170 changes: 170 additions & 0 deletions sdk/nodejs/automation/remoteStack.ts
@@ -0,0 +1,170 @@
// 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 { EngineEvent } from "./events";
import { LocalWorkspace } from "./localWorkspace";
import {
DestroyResult,
OutputMap,
PreviewResult,
RefreshResult,
Stack,
UpdateSummary,
UpResult,
} from "./stack";
import { Deployment } from "./workspace";

/**
* RemoteStack is an isolated, independencly configurable instance of a Pulumi program that is
* operated on remotely (up/preview/refresh/destroy).
*/
export class RemoteStack {
/** @internal */
static create(stack: Stack): RemoteStack {
return new RemoteStack(stack);
}

private constructor(private readonly stack: Stack) {
const ws = stack.workspace;
if (!(ws instanceof LocalWorkspace)) {
throw new Error("expected workspace to be an instance of LocalWorkspace");
}
}

/**
* The name identifying the Stack.
*/
get name(): string {
return this.stack.name;
}

/**
* Creates or updates the resources in a stack by executing the program in the Workspace.
* https://www.pulumi.com/docs/reference/cli/pulumi_up/
* This operation runs remotely.
*
* @param opts Options to customize the behavior of the update.
*/
up(opts?: RemoteUpOptions): Promise<UpResult> {
return this.stack.up(opts);
}

/**
* Performs a dry-run update to a stack, returning pending changes.
* https://www.pulumi.com/docs/reference/cli/pulumi_preview/
* This operation runs remotely.
*
* @param opts Options to customize the behavior of the preview.
*/
preview(opts?: RemotePreviewOptions): Promise<PreviewResult> {
return this.stack.preview(opts);
}

/**
* Compares the current stack鈥檚 resource state with the state known to exist in the actual
* cloud provider. Any such changes are adopted into the current stack.
* This operation runs remotely.
*
* @param opts Options to customize the behavior of the refresh.
*/
refresh(opts?: RemoteRefreshOptions): Promise<RefreshResult> {
return this.stack.refresh(opts);
}

/**
* Destroy deletes all resources in a stack, leaving all history and configuration intact.
* This operation runs remotely.
*
* @param opts Options to customize the behavior of the destroy.
*/
destroy(opts?: RemoteDestroyOptions): Promise<DestroyResult> {
return this.stack.destroy(opts);
}

/**
* Gets the current set of Stack outputs from the last Stack.up().
*/
outputs(): Promise<OutputMap> {
return this.stack.outputs();
}

/**
* Returns a list summarizing all previous and current results from Stack lifecycle operations
* (up/preview/refresh/destroy).
*/
history(pageSize?: number, page?: number): Promise<UpdateSummary[]> {
// TODO: Find a way to allow showSecrets as an option that doesn't require loading the project.
return this.stack.history(pageSize, page, false);
}

/**
* Cancel 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.
* This command is not supported for local backends.
*/
cancel(): Promise<void> {
return this.stack.cancel();
}

/**
* exportStack exports the deployment state of the stack.
* This can be combined with Stack.importStack to edit a stack's state (such as recovery from failed deployments).
*/
exportStack(): Promise<Deployment> {
return this.stack.exportStack();
}

/**
* importStack imports the specified deployment state into a pre-existing stack.
* This can be combined with Stack.exportStack to edit a stack's state (such as recovery from failed deployments).
*
* @param state the stack state to import.
*/
importStack(state: Deployment): Promise<void> {
return this.stack.importStack(state);
}
}

/**
* Options controlling the behavior of a RemoteStack.up() operation.
*/
export interface RemoteUpOptions {
onOutput?: (out: string) => void;
onEvent?: (event: EngineEvent) => void;
}

/**
* Options controlling the behavior of a RemoteStack.preview() operation.
*/
export interface RemotePreviewOptions {
onOutput?: (out: string) => void;
onEvent?: (event: EngineEvent) => void;
}

/**
* Options controlling the behavior of a RemoteStack.refresh() operation.
*/
export interface RemoteRefreshOptions {
onOutput?: (out: string) => void;
onEvent?: (event: EngineEvent) => void;
}

/**
* Options controlling the behavior of a RemoteStack.destroy() operation.
*/
export interface RemoteDestroyOptions {
onOutput?: (out: string) => void;
onEvent?: (event: EngineEvent) => void;
}

0 comments on commit adde4aa

Please sign in to comment.