From 4873a7796b3005886e94ea559d9b32d5b74ba5d8 Mon Sep 17 00:00:00 2001 From: Justin Van Patten Date: Wed, 19 Oct 2022 16:35:46 -0700 Subject: [PATCH] [auto/nodejs] Support for remote operations --- ...nodejs--support-for-remote-operations.yaml | 4 + sdk/nodejs/automation/index.ts | 4 +- sdk/nodejs/automation/localWorkspace.ts | 144 +++++++++++++- sdk/nodejs/automation/remoteStack.ts | 170 ++++++++++++++++ sdk/nodejs/automation/remoteWorkspace.ts | 186 ++++++++++++++++++ sdk/nodejs/automation/stack.ts | 36 +++- .../tests/automation/remoteWorkspace.spec.ts | 79 ++++++++ sdk/nodejs/tsconfig.json | 3 + 8 files changed, 618 insertions(+), 8 deletions(-) create mode 100644 changelog/pending/20221027--auto-nodejs--support-for-remote-operations.yaml create mode 100644 sdk/nodejs/automation/remoteStack.ts create mode 100644 sdk/nodejs/automation/remoteWorkspace.ts create mode 100644 sdk/nodejs/tests/automation/remoteWorkspace.spec.ts diff --git a/changelog/pending/20221027--auto-nodejs--support-for-remote-operations.yaml b/changelog/pending/20221027--auto-nodejs--support-for-remote-operations.yaml new file mode 100644 index 000000000000..fa779e70efdf --- /dev/null +++ b/changelog/pending/20221027--auto-nodejs--support-for-remote-operations.yaml @@ -0,0 +1,4 @@ +changes: +- type: feat + scope: auto/nodejs + description: Support for remote operations diff --git a/sdk/nodejs/automation/index.ts b/sdk/nodejs/automation/index.ts index 44b33518d0a9..8fff1f0777b1 100644 --- a/sdk/nodejs/automation/index.ts +++ b/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. @@ -21,3 +21,5 @@ export * from "./projectSettings"; export * from "./localWorkspace"; export * from "./workspace"; export * from "./events"; +export * from "./remoteStack"; +export * from "./remoteWorkspace"; diff --git a/sdk/nodejs/automation/localWorkspace.ts b/sdk/nodejs/automation/localWorkspace.ts index 5c64ea841562..14e3111f8b82 100644 --- a/sdk/nodejs/automation/localWorkspace.ts +++ b/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. @@ -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"; @@ -79,6 +80,27 @@ export class LocalWorkspace implements Workspace { return this._pulumiVersion.toString(); } private ready: Promise; + + /** + * 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. @@ -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 }; } @@ -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); } /** @@ -371,7 +401,15 @@ export class LocalWorkspace implements Workspace { * @param stackName The stack to select. */ async selectStack(stackName: string): Promise { - 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. @@ -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[], @@ -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; + } } /** @@ -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 } }; } /** diff --git a/sdk/nodejs/automation/remoteStack.ts b/sdk/nodejs/automation/remoteStack.ts new file mode 100644 index 000000000000..8261dda3c159 --- /dev/null +++ b/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 { + 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 { + return this.stack.preview(opts); + } + + /** + * Compares the current stack’s 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 { + 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 { + return this.stack.destroy(opts); + } + + /** + * Gets the current set of Stack outputs from the last Stack.up(). + */ + outputs(): Promise { + 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 { + // 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 { + 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 { + 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 { + 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; +} diff --git a/sdk/nodejs/automation/remoteWorkspace.ts b/sdk/nodejs/automation/remoteWorkspace.ts new file mode 100644 index 000000000000..fa7c3e024805 --- /dev/null +++ b/sdk/nodejs/automation/remoteWorkspace.ts @@ -0,0 +1,186 @@ +// 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 { LocalWorkspace, LocalWorkspaceOptions } from "./localWorkspace"; +import { RemoteStack } from "./remoteStack"; +import { Stack } from "./stack"; + +/** + * RemoteWorkspace is the execution context containing a single remote Pulumi project. + */ +export class RemoteWorkspace { + /** + * PREVIEW: Creates a Stack backed by a RemoteWorkspace with source code from the specified Git repository. + * Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + * + * @param args A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + * @param opts Additional customizations to be applied to the Workspace. + */ + static async createStack(args: RemoteGitProgramArgs, opts?: RemoteWorkspaceOptions): Promise { + const ws = await createLocalWorkspace(args, opts); + const stack = await Stack.create(args.stackName, ws); + return RemoteStack.create(stack); + } + + /** + * PREVIEW: Selects an existing Stack backed by a RemoteWorkspace with source code from the specified Git + * repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + * + * @param args A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + * @param opts Additional customizations to be applied to the Workspace. + */ + static async selectStack(args: RemoteGitProgramArgs, opts?: RemoteWorkspaceOptions): Promise { + const ws = await createLocalWorkspace(args, opts); + const stack = await Stack.select(args.stackName, ws); + return RemoteStack.create(stack); + } + /** + * PREVIEW: Creates or selects an existing Stack backed by a RemoteWorkspace with source code from the specified + * Git repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + * + * @param args A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + * @param opts Additional customizations to be applied to the Workspace. + */ + static async createOrSelectStack(args: RemoteGitProgramArgs, opts?: RemoteWorkspaceOptions): Promise { + const ws = await createLocalWorkspace(args, opts); + const stack = await Stack.createOrSelect(args.stackName, ws); + return RemoteStack.create(stack); + } + + private constructor() {} // eslint-disable-line @typescript-eslint/no-empty-function +} + +/** + * Description of a stack backed by a remote Pulumi program in a Git repository. + */ +export interface RemoteGitProgramArgs { + /** + * The name of the associated Stack + */ + stackName: string; + + /** + * The URL of the repository. + */ + url: string; + + /** + * Optional path relative to the repo root specifying location of the Pulumi program. + */ + projectPath?: string; + + /** + * Optional branch to checkout. + */ + branch?: string; + + /** + * Optional commit to checkout. + */ + commitHash?: string; + + /** + * Authentication options for the repository. + */ + auth?: RemoteGitAuthArgs; +} + +/** + * Authentication options for the repository that can be specified for a private Git repo. + * There are three different authentication paths: + * - Personal accesstoken + * - SSH private key (and its optional password) + * - Basic auth username and password + * + * Only one authentication path is valid. + */ +export interface RemoteGitAuthArgs { + /** + * The absolute path to a private key for access to the git repo. + */ + sshPrivateKeyPath?: string; + + /** + * The (contents) private key for access to the git repo. + */ + sshPrivateKey?: string; + + /** + * The password that pairs with a username or as part of an SSH Private Key. + */ + password?: string; + + /** + * PersonalAccessToken is a Git personal access token in replacement of your password. + */ + personalAccessToken?: string; + + /** + * Username is the username to use when authenticating to a git repository + */ + username?: string; +} + +/** + * Extensibility options to configure a RemoteWorkspace. + */ +export interface RemoteWorkspaceOptions { + /** + * Environment values scoped to the remote workspace. These will be passed to remote operations. + */ + envVars?: { [key: string]: string | { secret: string } }; + + /** + * An optional list of arbitrary commands to run before a remote Pulumi operation is invoked. + */ + preRunCommands?: string[]; +} + +async function createLocalWorkspace(args: RemoteGitProgramArgs, opts?: RemoteWorkspaceOptions): Promise { + if (!isFullyQualifiedStackName(args.stackName)) { + throw new Error(`"${args.stackName}" stack name must be fully qualified`); + } + + if (!args.url) { + throw new Error("url is required."); + } + if (args.commitHash && args.branch) { + throw new Error("commitHash and branch cannot both be specified."); + } + if (!args.commitHash && !args.branch) { + throw new Error("at least commitHash or branch are required."); + } + if (args.auth) { + if (args.auth.sshPrivateKey && args.auth.sshPrivateKeyPath) { + throw new Error("sshPrivateKey and sshPrivateKeyPath cannot both be specified."); + } + } + + const localOpts: LocalWorkspaceOptions = { + remote: true, + remoteGitProgramArgs: args, + remoteEnvVars: opts?.envVars, + remotePreRunCommands: opts?.preRunCommands, + }; + return await LocalWorkspace.create(localOpts); +} + +/** @internal exported only so it can be tested */ +export function isFullyQualifiedStackName(stackName: string): boolean { + if (!stackName) { + return false; + } + const split = stackName.split("/"); + return split.length === 3 && !!split[0] && !!split[1] && !!split[2]; +} diff --git a/sdk/nodejs/automation/stack.ts b/sdk/nodejs/automation/stack.ts index 806b1ffd86f4..9dbd021d6f01 100644 --- a/sdk/nodejs/automation/stack.ts +++ b/sdk/nodejs/automation/stack.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. @@ -28,6 +28,7 @@ import { StackAlreadyExistsError } from "./errors"; import { EngineEvent, SummaryEvent } from "./events"; import { LanguageServer, maxRPCMessageSize } from "./server"; import { Deployment, PulumiFn, Workspace } from "./workspace"; +import { LocalWorkspace } from "./localWorkspace"; const langrpc = require("../proto/language_grpc_pb.js"); @@ -152,6 +153,8 @@ Event: ${line}\n${e.toString()}`); let kind = execKind.local; let program = this.workspace.program; + args.push(...this.remoteArgs()); + if (opts) { if (opts.program) { program = opts.program; @@ -255,7 +258,9 @@ Event: ${line}\n${e.toString()}`); // TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050 const outputs = await this.outputs(); - const summary = await this.info(opts?.showSecrets); + // 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, @@ -275,6 +280,8 @@ Event: ${line}\n${e.toString()}`); let kind = execKind.local; let program = this.workspace.program; + args.push(...this.remoteArgs()); + if (opts) { if (opts.program) { program = opts.program; @@ -396,6 +403,8 @@ Event: ${line}\n${e.toString()}`); async refresh(opts?: RefreshOptions): Promise { const args = ["refresh", "--yes", "--skip-preview"]; + args.push(...this.remoteArgs()); + if (opts) { if (opts.message) { args.push("--message", opts.message); @@ -437,7 +446,9 @@ Event: ${line}\n${e.toString()}`); const [refResult, logResult] = await Promise.all([refPromise, logPromise]); await cleanUp(logFile, logResult); - const summary = await this.info(opts?.showSecrets); + // 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, @@ -452,6 +463,8 @@ Event: ${line}\n${e.toString()}`); async destroy(opts?: DestroyOptions): Promise { const args = ["destroy", "--yes", "--skip-preview"]; + args.push(...this.remoteArgs()); + if (opts) { if (opts.message) { args.push("--message", opts.message); @@ -493,7 +506,9 @@ Event: ${line}\n${e.toString()}`); const [desResult, logResult] = await Promise.all([desPromise, logPromise]); await cleanUp(logFile, logResult); - const summary = await this.info(opts?.showSecrets); + // 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: desResult.stdout, stderr: desResult.stderr, @@ -622,6 +637,9 @@ Event: ${line}\n${e.toString()}`); 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; @@ -633,6 +651,16 @@ Event: ${line}\n${e.toString()}`); 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() : []; + } } function applyGlobalOpts(opts: GlobalOpts, args: string[]) { diff --git a/sdk/nodejs/tests/automation/remoteWorkspace.spec.ts b/sdk/nodejs/tests/automation/remoteWorkspace.spec.ts new file mode 100644 index 000000000000..8479cb64622d --- /dev/null +++ b/sdk/nodejs/tests/automation/remoteWorkspace.spec.ts @@ -0,0 +1,79 @@ +// 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 assert from "assert"; + +import { isFullyQualifiedStackName } from "../../automation"; + +describe("isFullyQualifiedStackName", () => { + const tests = [ + { + name: "fully qualified", + input: "owner/project/stack", + expected: true, + }, + { + name: "undefined", + input: undefined, + expected: false, + }, + { + name: "null", + input: null, + expected: false, + }, + { + name: "empty", + input: "", + expected: false, + }, + { + name: "name", + input: "name", + expected: false, + }, + { + name: "name & owner", + input: "owner/name", + expected: false, + }, + { + name: "sep", + input: "/", + expected: false, + }, + { + name: "two seps", + input: "//", + expected: false, + }, + { + name: "three seps", + input: "///", + expected: false, + }, + { + name: "invalid", + input: "owner/project/stack/wat", + expected: false, + }, + ]; + + tests.forEach(test => { + it(`${test.name}`, () => { + const actual = isFullyQualifiedStackName(test.input!); + assert.strictEqual(actual, test.expected); + }); + }); +}); diff --git a/sdk/nodejs/tsconfig.json b/sdk/nodejs/tsconfig.json index 0f7205a14fe4..8804972f62b0 100644 --- a/sdk/nodejs/tsconfig.json +++ b/sdk/nodejs/tsconfig.json @@ -78,6 +78,8 @@ "automation/localWorkspace.ts", "automation/minimumVersion.ts", "automation/projectSettings.ts", + "automation/remoteStack.ts", + "automation/remoteWorkspace.ts", "automation/stackSettings.ts", "automation/server.ts", "automation/stack.ts", @@ -105,6 +107,7 @@ "tests/automation/cmd.spec.ts", "tests/automation/localWorkspace.spec.ts", + "tests/automation/remoteWorkspace.spec.ts", "tests_with_mocks/mocks.spec.ts" ]