// 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 { fullyQualifiedStackName, isFullyQualifiedStackName, LocalWorkspace, LocalWorkspaceOptions, RemoteGitAuthArgs, RemoteGitProgramArgs, RemoteStack, RemoteWorkspace, RemoteWorkspaceOptions, } from "../../automation"; import { getTestOrg, getTestSuffix } from "./util"; const testRepo = "https://github.com/pulumi/test-repo.git"; describe("RemoteWorkspace", () => { describe("remote cmd args", () => { const tests: { name: string; opts: LocalWorkspaceOptions; expected: string[]; }[] = [ { name: "empty", opts: {}, expected: [], }, { name: "just remote", opts: { remote: true, }, expected: ["--remote"], }, { name: "url", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", }, }, expected: ["--remote", "foo"], }, { name: "path", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", projectPath: "mypath", }, }, expected: ["--remote", "foo", "--remote-git-repo-dir", "mypath"], }, { name: "branch", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", branch: "mybranch", }, }, expected: ["--remote", "foo", "--remote-git-branch", "mybranch"], }, { name: "commit", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", commitHash: "mycommit", }, }, expected: ["--remote", "foo", "--remote-git-commit", "mycommit"], }, { name: "auth access token", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", auth: { personalAccessToken: "mytoken", }, }, }, expected: ["--remote", "foo", "--remote-git-auth-access-token", "mytoken"], }, { name: "auth ssh key", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", auth: { sshPrivateKey: "mykey", }, }, }, expected: ["--remote", "foo", "--remote-git-auth-ssh-private-key", "mykey"], }, { name: "auth ssh key path", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", auth: { sshPrivateKeyPath: "mykeypath", }, }, }, expected: ["--remote", "foo", "--remote-git-auth-ssh-private-key-path", "mykeypath"], }, { name: "auth ssh password", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", auth: { username: "myuser", password: "mypass", }, }, }, expected: [ "--remote", "foo", "--remote-git-auth-password", "mypass", "--remote-git-auth-username", "myuser", ], }, { name: "env", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", }, remoteEnvVars: { foo: "bar", }, }, expected: ["--remote", "foo", "--remote-env", "foo=bar"], }, { name: "env secret", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", }, remoteEnvVars: { foo: { secret: "bar" }, }, }, expected: ["--remote", "foo", "--remote-env-secret", "foo=bar"], }, { name: "pre-run command", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", }, remotePreRunCommands: ["whoami"], }, expected: ["--remote", "foo", "--remote-pre-run-command", "whoami"], }, { name: "skip install dependencies", opts: { remote: true, remoteGitProgramArgs: { stackName: "stack", url: "foo", }, remoteSkipInstallDependencies: true, }, expected: ["--remote", "foo", "--remote-skip-install-dependencies"], }, ]; tests.forEach((test) => { it(`${test.name}`, async () => { const ws = await LocalWorkspace.create(test.opts); const actual = ws.remoteArgs(); assert.deepStrictEqual(actual, test.expected); }); }); }); describe("selectStack", () => { describe("throws appropriate errors", () => testErrors(RemoteWorkspace.selectStack)); }); describe("createStack", () => { describe("throws appropriate errors", () => testErrors(RemoteWorkspace.createStack)); it(`runs through the stack lifecycle`, async function () { // This test requires the service with access to Pulumi Deployments. // Set PULUMI_ACCESS_TOKEN to an access token with access to Pulumi Deployments // and set PULUMI_TEST_DEPLOYMENTS_API to any value to enable the test. if (!process.env.PULUMI_ACCESS_TOKEN) { this.skip(); return; } if (!process.env.PULUMI_TEST_DEPLOYMENTS_API) { this.skip(); return; } await testLifecycle(RemoteWorkspace.createStack); }); }); describe("createOrSelectStack", () => { describe("throws appropriate errors", () => testErrors(RemoteWorkspace.createOrSelectStack)); it(`runs through the stack lifecycle`, async function () { // This test requires the service with access to Pulumi Deployments. // Set PULUMI_ACCESS_TOKEN to an access token with access to Pulumi Deployments // and set PULUMI_TEST_DEPLOYMENTS_API to any value to enable the test. if (!process.env.PULUMI_ACCESS_TOKEN) { this.skip(); return; } if (!process.env.PULUMI_TEST_DEPLOYMENTS_API) { this.skip(); return; } await testLifecycle(RemoteWorkspace.createOrSelectStack); }); }); }); function testErrors(fn: (args: RemoteGitProgramArgs, opts?: RemoteWorkspaceOptions) => Promise<RemoteStack>) { const stack = "owner/project/stack"; const tests: { name: string; stackName: string; url: string; branch?: string; commitHash?: string; auth?: RemoteGitAuthArgs; error: string; }[] = [ { name: "stack empty", stackName: "", url: "", error: `stack name "" must be fully qualified.`, }, { name: "stack just name", stackName: "name", url: "", error: `stack name "name" must be fully qualified.`, }, { name: "stack just name & owner", stackName: "owner/name", url: "", error: `stack name "owner/name" must be fully qualified.`, }, { name: "stack just sep", stackName: "/", url: "", error: `stack name "/" must be fully qualified.`, }, { name: "stack just two seps", stackName: "//", url: "", error: `stack name "//" must be fully qualified.`, }, { name: "stack just three seps", stackName: "///", url: "", error: `stack name "///" must be fully qualified.`, }, { name: "stack invalid", stackName: "owner/project/stack/wat", url: "", error: `stack name "owner/project/stack/wat" must be fully qualified.`, }, { name: "no url", stackName: stack, url: "", error: `url is required.`, }, { name: "no branch or commit", stackName: stack, url: testRepo, error: `either branch or commitHash is required.`, }, { name: "both branch and commit", stackName: stack, url: testRepo, branch: "branch", commitHash: "commit", error: `branch and commitHash cannot both be specified.`, }, { name: "both ssh private key and path", stackName: stack, url: testRepo, branch: "branch", auth: { sshPrivateKey: "key", sshPrivateKeyPath: "path", }, error: `sshPrivateKey and sshPrivateKeyPath cannot both be specified.`, }, ]; tests.forEach((test) => { it(`${test.name}`, async () => { const { stackName, url, branch, commitHash, auth } = test; await assert.rejects( async () => { await fn({ stackName, url, branch, commitHash, auth }); }, { message: test.error, }, ); }); }); } async function testLifecycle(fn: (args: RemoteGitProgramArgs, opts?: RemoteWorkspaceOptions) => Promise<RemoteStack>) { const stackName = fullyQualifiedStackName(getTestOrg(), "go_remote_proj", `int_test${getTestSuffix()}`); const stack = await fn( { stackName, url: testRepo, branch: "refs/heads/master", projectPath: "goproj", }, { preRunCommands: [ `pulumi config set bar abc --stack ${stackName}`, `pulumi config set --secret buzz secret --stack ${stackName}`, ], skipInstallDependencies: true, }, ); // pulumi up const upRes = await stack.up(); assert.strictEqual(Object.keys(upRes.outputs).length, 3); assert.strictEqual(upRes.outputs["exp_static"].value, "foo"); assert.strictEqual(upRes.outputs["exp_static"].secret, false); assert.strictEqual(upRes.outputs["exp_cfg"].value, "abc"); assert.strictEqual(upRes.outputs["exp_cfg"].secret, false); assert.strictEqual(upRes.outputs["exp_secret"].value, "secret"); assert.strictEqual(upRes.outputs["exp_secret"].secret, true); assert.strictEqual(upRes.summary.kind, "update"); assert.strictEqual(upRes.summary.result, "succeeded"); // pulumi preview const preRes = await stack.preview(); assert.strictEqual(preRes.changeSummary.same, 1); // pulumi refresh const refRes = await stack.refresh(); assert.strictEqual(refRes.summary.kind, "refresh"); assert.strictEqual(refRes.summary.result, "succeeded"); // pulumi destroy const destroyRes = await stack.destroy(); assert.strictEqual(destroyRes.summary.kind, "destroy"); assert.strictEqual(destroyRes.summary.result, "succeeded"); await (await LocalWorkspace.create({})).removeStack(stackName); } 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); }); }); });