Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[auto/nodejs] Support for remote operations #11170

Merged
merged 1 commit into from Oct 28, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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";
144 changes: 141 additions & 3 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 All @@ -371,7 +401,15 @@ export class LocalWorkspace implements Workspace {
* @param stackName The stack to select.
*/
async selectStack(stackName: string): Promise<void> {
await this.runPulumiCmd(["stack", "select", stackName]);
// If this is a remote workspace, we don't want to actually select the stack (which would modify global state);
// but we will ensure the stack exists by calling `pulumi stack`.
const args = ["stack"];
if (!this.isRemote) {
args.push("select");
}
args.push("--stack", stackName);

await this.runPulumiCmd(args);
}
/**
* Deletes the stack and all associated configuration and history.
Expand Down Expand Up @@ -611,6 +649,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 +667,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 +799,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