pulumi/sdk/nodejs/tests/automation/localWorkspace.spec.ts

1697 lines
69 KiB
TypeScript

// Copyright 2016-2021, 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 * as semver from "semver";
import * as tmp from "tmp";
import * as upath from "upath";
import * as fs from "fs";
import {
CommandResult,
ConfigMap,
EngineEvent,
fullyQualifiedStackName,
LocalWorkspace,
LocalWorkspaceOptions,
OutputMap,
ProjectSettings,
ProjectRuntime,
PulumiCommand,
Stack,
} from "../../automation";
import { ComponentResource, ComponentResourceOptions, Config, output } from "../../index";
import { getTestOrg, getTestSuffix } from "./util";
const versionRegex = /(\d+\.)(\d+\.)(\d+)(-.*)?/;
const userAgent = "pulumi/pulumi/test";
describe("LocalWorkspace", () => {
it(`projectSettings from yaml/yml/json`, async () => {
for (const ext of ["yaml", "yml", "json"]) {
const ws = await LocalWorkspace.create(
withTestBackend(
{ workDir: upath.joinSafe(__dirname, "data", ext) },
"testproj",
"A minimal Go Pulumi program",
"go",
),
);
const settings = await ws.projectSettings();
assert.strictEqual(settings.name, "testproj");
assert.strictEqual(settings.runtime, "go");
assert.strictEqual(settings.description, "A minimal Go Pulumi program");
}
});
it(`stackSettings from yaml/yml/json`, async () => {
for (const ext of ["yaml", "yml", "json"]) {
const ws = await LocalWorkspace.create(
withTestBackend({ workDir: upath.joinSafe(__dirname, "data", ext) }),
);
const settings = await ws.stackSettings("dev");
assert.strictEqual(settings.secretsProvider, "abc");
assert.strictEqual(settings.config!["plain"], "plain");
assert.strictEqual(settings.config!["secure"].secure, "secret");
await ws.saveStackSettings("dev", settings);
assert.strictEqual(settings.secretsProvider, "abc");
}
});
it(`fails gracefully for missing local workspace workDir`, async () => {
try {
const ws = await LocalWorkspace.create(withTestBackend({ workDir: "invalid-missing-workdir" }));
assert.fail("expected create with invalid workDir to throw");
} catch (err) {
assert.strictEqual(
err.toString(),
"Error: Invalid workDir passed to local workspace: 'invalid-missing-workdir' does not exist",
);
}
});
it(`adds/removes/lists plugins successfully`, async () => {
const ws = await LocalWorkspace.create(withTestBackend({}));
await ws.installPlugin("aws", "v3.0.0");
// See https://github.com/pulumi/pulumi/issues/11013 for why this is disabled
//await ws.installPluginFromServer("scaleway", "v1.2.0", "github://api.github.com/lbrlabs");
await ws.removePlugin("aws", "3.0.0");
await ws.listPlugins();
});
it(`create/select/remove LocalWorkspace stack`, async () => {
const projectName = "node_test";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
await ws.createStack(stackName);
await ws.selectStack(stackName);
await ws.removeStack(stackName);
});
it(`create/select/createOrSelect Stack`, async () => {
const projectName = "node_test";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
await Stack.create(stackName, ws);
await Stack.select(stackName, ws);
await Stack.createOrSelect(stackName, ws);
await ws.removeStack(stackName);
});
describe("Tag methods: get/set/remove/list", () => {
const projectName = "testProjectName";
const runtime = "nodejs";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const projectSettings: ProjectSettings = {
name: projectName,
runtime,
};
let workspace: LocalWorkspace;
beforeEach(async () => {
workspace = await LocalWorkspace.create(
withTestBackend({
projectSettings: projectSettings,
}),
);
await workspace.createStack(stackName);
});
it("lists tag values", async () => {
if (!process.env.PULUMI_ACCESS_TOKEN) {
console.log('Skipping "list tag values values" test');
// Skip the test because the local backend doesn't support tags
return;
}
const result = await workspace.listTags(stackName);
assert.strictEqual(result["pulumi:project"], projectName);
assert.strictEqual(result["pulumi:runtime"], runtime);
});
it("sets and removes tag values", async () => {
if (!process.env.PULUMI_ACCESS_TOKEN) {
console.log('Skipping "sets and removes tag values" test');
// Skip the test because the local backend doesn't support tags
return;
}
// sets
await workspace.setTag(stackName, "foo", "bar");
const actualValue = await workspace.getTag(stackName, "foo");
assert.strictEqual(actualValue, "bar");
// removes
await workspace.removeTag(stackName, "foo");
const actualTags = await workspace.listTags(stackName);
assert.strictEqual(actualTags["foo"], undefined);
});
it("gets a single tag value", async () => {
if (!process.env.PULUMI_ACCESS_TOKEN) {
console.log('Skipping "gets a single tag value" test');
// Skip the test because the local backend doesn't support tags
return;
}
const actualValue = await workspace.getTag(stackName, "pulumi:project");
assert.strictEqual(actualValue, actualValue.trim());
assert.strictEqual(actualValue, projectName);
});
afterEach(async () => {
await workspace.removeStack(stackName);
});
});
describe("ListStack Methods", async () => {
describe("ListStacks", async () => {
const stackJson = `[
{
"name": "testorg1/testproj1/teststack1",
"current": false,
"url": "https://app.pulumi.com/testorg1/testproj1/teststack1"
},
{
"name": "testorg1/testproj1/teststack2",
"current": false,
"url": "https://app.pulumi.com/testorg1/testproj1/teststack2"
}
]`;
it(`should handle stacks correctly for listStacks`, async () => {
const mockWithReturnedStacks = {
command: "pulumi",
version: null,
run: async (args: string[], cwd: string, additionalEnv: { [key: string]: string }) => {
return new CommandResult(stackJson, "", 0);
},
};
const workspace = await LocalWorkspace.create(
withTestBackend({ pulumiCommand: mockWithReturnedStacks }),
);
const stacks = await workspace.listStacks();
assert.strictEqual(stacks.length, 2);
assert.strictEqual(stacks[0].name, "testorg1/testproj1/teststack1");
assert.strictEqual(stacks[0].current, false);
assert.strictEqual(stacks[0].url, "https://app.pulumi.com/testorg1/testproj1/teststack1");
assert.strictEqual(stacks[1].name, "testorg1/testproj1/teststack2");
assert.strictEqual(stacks[1].current, false);
assert.strictEqual(stacks[1].url, "https://app.pulumi.com/testorg1/testproj1/teststack2");
});
it(`should use correct args for listStacks`, async () => {
let capturedArgs: string[] = [];
const mockPulumiCommand = {
command: "pulumi",
version: null,
run: async (args: string[], cwd: string, additionalEnv: { [key: string]: string }) => {
capturedArgs = args;
return new CommandResult(stackJson, "", 0);
},
};
const workspace = await LocalWorkspace.create(
withTestBackend({
pulumiCommand: mockPulumiCommand,
}),
);
await workspace.listStacks();
assert.deepStrictEqual(capturedArgs, ["stack", "ls", "--json"]);
});
});
describe("ListStacks with all", async () => {
const stackJson = `[
{
"name": "testorg1/testproj1/teststack1",
"current": false,
"url": "https://app.pulumi.com/testorg1/testproj1/teststack1"
},
{
"name": "testorg1/testproj2/teststack2",
"current": false,
"url": "https://app.pulumi.com/testorg1/testproj2/teststack2"
}
]`;
it(`should handle stacks correctly for listStacks when all is set`, async () => {
const mockWithReturnedStacks = {
command: "pulumi",
version: null,
run: async () => new CommandResult(stackJson, "", 0),
};
const workspace = await LocalWorkspace.create(
withTestBackend({
pulumiCommand: mockWithReturnedStacks,
}),
);
const stacks = await workspace.listStacks({ all: true });
assert.strictEqual(stacks.length, 2);
assert.strictEqual(stacks[0].name, "testorg1/testproj1/teststack1");
assert.strictEqual(stacks[0].current, false);
assert.strictEqual(stacks[0].url, "https://app.pulumi.com/testorg1/testproj1/teststack1");
assert.strictEqual(stacks[1].name, "testorg1/testproj2/teststack2");
assert.strictEqual(stacks[1].current, false);
assert.strictEqual(stacks[1].url, "https://app.pulumi.com/testorg1/testproj2/teststack2");
});
it(`should use correct args for listStacks when all is set`, async () => {
let capturedArgs: string[] = [];
const mockPuluiCommand = {
command: "pulumi",
version: null,
run: async (args: string[], cwd: string, additionalEnv: { [key: string]: string }) => {
capturedArgs = args;
return new CommandResult(stackJson, "", 0);
},
};
const workspace = await LocalWorkspace.create(
withTestBackend({
pulumiCommand: mockPuluiCommand,
}),
);
await workspace.listStacks({ all: true });
assert.deepStrictEqual(capturedArgs, ["stack", "ls", "--json", "--all"]);
});
});
});
it(`Environment functions`, async function () {
// Skipping test because the required environments are in the moolumi org.
if (getTestOrg() !== "moolumi") {
this.skip();
return;
}
const projectName = "node_env_test";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await Stack.create(stackName, ws);
// Adding non-existent env should fail.
await assert.rejects(
stack.addEnvironments("non-existent-env"),
"stack.addEnvironments('non-existent-env') did not reject",
);
// Adding existing envs should succeed.
await stack.addEnvironments("automation-api-test-env", "automation-api-test-env-2");
let envs = await stack.listEnvironments();
assert.deepStrictEqual(envs, ["automation-api-test-env", "automation-api-test-env-2"]);
const config = await stack.getAllConfig();
assert.strictEqual(config["node_env_test:new_key"].value, "test_value");
assert.strictEqual(config["node_env_test:also"].value, "business");
// Removing existing env should succeed.
await stack.removeEnvironment("automation-api-test-env");
envs = await stack.listEnvironments();
assert.deepStrictEqual(envs, ["automation-api-test-env-2"]);
const alsoConfig = await stack.getConfig("also");
assert.strictEqual(alsoConfig.value, "business");
await assert.rejects(stack.getConfig("new_key"), "stack.getConfig('new_key') did not reject");
await stack.removeEnvironment("automation-api-test-env-2");
envs = await stack.listEnvironments();
assert.strictEqual(envs.length, 0);
await assert.rejects(stack.getConfig("also"), "stack.getConfig('also') did not reject");
await ws.removeStack(stackName);
});
it(`Config`, async () => {
const projectName = "node_test";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await Stack.create(stackName, ws);
const config = {
plain: { value: "abc" },
secret: { value: "def", secret: true },
};
let caught = 0;
const plainKey = normalizeConfigKey("plain", projectName);
const secretKey = normalizeConfigKey("secret", projectName);
try {
await stack.getConfig(plainKey);
} catch (error) {
caught++;
}
assert.strictEqual(caught, 1, "expected config get on empty value to throw");
let values = await stack.getAllConfig();
assert.strictEqual(Object.keys(values).length, 0, "expected stack config to be empty");
await stack.setAllConfig(config);
values = await stack.getAllConfig();
assert.strictEqual(values[plainKey].value, "abc");
assert.strictEqual(values[plainKey].secret, false);
assert.strictEqual(values[secretKey].value, "def");
assert.strictEqual(values[secretKey].secret, true);
await stack.removeConfig("plain");
values = await stack.getAllConfig();
assert.strictEqual(Object.keys(values).length, 1, "expected stack config to have 1 value");
await stack.setConfig("foo", { value: "bar" });
values = await stack.getAllConfig();
assert.strictEqual(Object.keys(values).length, 2, "expected stack config to have 2 values");
await ws.removeStack(stackName);
});
it(`config_flag_like`, async () => {
const projectName = "config_flag_like";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await Stack.create(stackName, ws);
await stack.setConfig("key", { value: "-value" });
await stack.setConfig("secret-key", { value: "-value", secret: true });
const values = await stack.getAllConfig();
assert.strictEqual(values["config_flag_like:key"].value, "-value");
assert.strictEqual(values["config_flag_like:key"].secret, false);
assert.strictEqual(values["config_flag_like:secret-key"].value, "-value");
assert.strictEqual(values["config_flag_like:secret-key"].secret, true);
await stack.setAllConfig({
key: { value: "-value2" },
"secret-key": { value: "-value2", secret: true },
});
const values2 = await stack.getAllConfig();
assert.strictEqual(values2["config_flag_like:key"].value, "-value2");
assert.strictEqual(values2["config_flag_like:key"].secret, false);
assert.strictEqual(values2["config_flag_like:secret-key"].value, "-value2");
assert.strictEqual(values2["config_flag_like:secret-key"].secret, true);
});
it(`Config path`, async () => {
const projectName = "node_test";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await Stack.create(stackName, ws);
// test backward compatibility
await stack.setConfig("key1", { value: "value1" });
// test new flag without subPath
await stack.setConfig("key2", { value: "value2" }, false);
// test new flag with subPath
await stack.setConfig("key3.subKey1", { value: "value3" }, true);
// test secret
await stack.setConfig("key4", { value: "value4", secret: true });
// test subPath and key as secret
await stack.setConfig("key5.subKey1", { value: "value5", secret: true }, true);
// test string with dots
await stack.setConfig("key6.subKey1", { value: "value6", secret: true });
// test string with dots
await stack.setConfig("key7.subKey1", { value: "value7", secret: true }, false);
// test subPath
await stack.setConfig("key7.subKey2", { value: "value8" }, true);
// test subPath
await stack.setConfig("key7.subKey3", { value: "value9" }, true);
// test backward compatibility
const cv1 = await stack.getConfig("key1");
assert.strictEqual(cv1.value, "value1");
assert.strictEqual(cv1.secret, false);
// test new flag without subPath
const cv2 = await stack.getConfig("key2", false);
assert.strictEqual(cv2.value, "value2");
assert.strictEqual(cv2.secret, false);
// test new flag with subPath
const cv3 = await stack.getConfig("key3.subKey1", true);
assert.strictEqual(cv3.value, "value3");
assert.strictEqual(cv3.secret, false);
// test secret
const cv4 = await stack.getConfig("key4");
assert.strictEqual(cv4.value, "value4");
assert.strictEqual(cv4.secret, true);
// test subPath and key as secret
const cv5 = await stack.getConfig("key5.subKey1", true);
assert.strictEqual(cv5.value, "value5");
assert.strictEqual(cv5.secret, true);
// test string with dots
const cv6 = await stack.getConfig("key6.subKey1");
assert.strictEqual(cv6.value, "value6");
assert.strictEqual(cv6.secret, true);
// test string with dots
const cv7 = await stack.getConfig("key7.subKey1", false);
assert.strictEqual(cv7.value, "value7");
assert.strictEqual(cv7.secret, true);
// test string with dots
const cv8 = await stack.getConfig("key7.subKey2", true);
assert.strictEqual(cv8.value, "value8");
assert.strictEqual(cv8.secret, false);
// test string with dots
const cv9 = await stack.getConfig("key7.subKey3", true);
assert.strictEqual(cv9.value, "value9");
assert.strictEqual(cv9.secret, false);
await stack.removeConfig("key1");
await stack.removeConfig("key2", false);
await stack.removeConfig("key3", false);
await stack.removeConfig("key4", false);
await stack.removeConfig("key5", false);
await stack.removeConfig("key6.subKey1", false);
await stack.removeConfig("key7.subKey1", false);
const cfg = await stack.getAllConfig();
assert.strictEqual(cfg["node_test:key7"].value, '{"subKey2":"value8","subKey3":"value9"}');
await ws.removeStack(stackName);
});
// This test requires the existence of a Pulumi.dev.yaml file because we are reading the nested
// config from the file. This means we can't remove the stack at the end of the test.
// We should also not include secrets in this config, because the secret encryption is only valid within
// the context of a stack and org, and running this test in different orgs will fail if there are secrets.
it(`nested_config`, async () => {
const stackName = fullyQualifiedStackName(getTestOrg(), "nested_config", "dev");
const workDir = upath.joinSafe(__dirname, "data", "nested_config");
const stack = await LocalWorkspace.createOrSelectStack(
{ stackName, workDir },
withTestBackend({}, "nested_config"),
);
const allConfig = await stack.getAllConfig();
const outerVal = allConfig["nested_config:outer"];
assert.strictEqual(outerVal.secret, false);
assert.strictEqual(outerVal.value, '{"inner":"my_value","other":"something_else"}');
const listVal = allConfig["nested_config:myList"];
assert.strictEqual(listVal.secret, false);
assert.strictEqual(listVal.value, '["one","two","three"]');
const outer = await stack.getConfig("outer");
assert.strictEqual(outer.secret, false);
assert.strictEqual(outer.value, '{"inner":"my_value","other":"something_else"}');
const list = await stack.getConfig("myList");
assert.strictEqual(list.secret, false);
assert.strictEqual(list.value, '["one","two","three"]');
});
it(`can list stacks and currently selected stack`, async () => {
const projectName = `node_list_test${getTestSuffix()}`;
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackNamer = () => `int_test${getTestSuffix()}`;
const stackNames: string[] = [];
for (let i = 0; i < 2; i++) {
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, stackNamer());
stackNames[i] = stackName;
await Stack.create(stackName, ws);
const stackSummary = await ws.stack();
assert.strictEqual(stackSummary?.current, true);
const stacks = await ws.listStacks();
assert.strictEqual(stacks.length, i + 1);
}
for (const name of stackNames) {
await ws.removeStack(name);
}
});
it(`returns valid whoami result`, async () => {
const projectName = "node_test";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const whoAmIResult = await ws.whoAmI();
assert(whoAmIResult.user !== null);
assert(whoAmIResult.url !== null);
});
it(`stack status methods`, async () => {
const projectName = "node_test";
const projectSettings: ProjectSettings = {
name: projectName,
runtime: "nodejs",
};
const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await Stack.create(stackName, ws);
const history = await stack.history();
assert.strictEqual(history.length, 0);
const info = await stack.info();
assert.strictEqual(typeof info, "undefined");
await ws.removeStack(stackName);
});
// TODO[pulumi/pulumi#8220] understand why this test was flaky
xit(`runs through the stack lifecycle with a local program`, async () => {
const stackName = fullyQualifiedStackName(getTestOrg(), "testproj", `int_test${getTestSuffix()}`);
const workDir = upath.joinSafe(__dirname, "data", "testproj");
const stack = await LocalWorkspace.createStack({ stackName, workDir }, withTestBackend({}));
const config: ConfigMap = {
bar: { value: "abc" },
buzz: { value: "secret", secret: true },
};
await stack.setAllConfig(config);
// pulumi up
const upRes = await stack.up({ userAgent });
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({ userAgent });
assert.strictEqual(preRes.changeSummary.same, 1);
// pulumi refresh
const refRes = await stack.refresh({ userAgent });
assert.strictEqual(refRes.summary.kind, "refresh");
assert.strictEqual(refRes.summary.result, "succeeded");
// pulumi destroy
const destroyRes = await stack.destroy({ userAgent });
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
});
it(`runs through the stack lifecycle with a local dotnet program`, async () => {
const stackName = fullyQualifiedStackName(getTestOrg(), "testproj_dotnet", `int_test${getTestSuffix()}`);
const workDir = upath.joinSafe(__dirname, "data", "testproj_dotnet");
const stack = await LocalWorkspace.createStack(
{ stackName, workDir },
withTestBackend({}, "testproj_dotnet", "", "dotnet"),
);
// pulumi up
const upRes = await stack.up({ userAgent });
assert.strictEqual(Object.keys(upRes.outputs).length, 1);
assert.strictEqual(upRes.outputs["exp_static"].value, "foo");
assert.strictEqual(upRes.outputs["exp_static"].secret, false);
assert.strictEqual(upRes.summary.kind, "update");
assert.strictEqual(upRes.summary.result, "succeeded");
// pulumi preview
const preRes = await stack.preview({ userAgent });
assert.strictEqual(preRes.changeSummary.same, 1);
// pulumi refresh
const refRes = await stack.refresh({ userAgent });
assert.strictEqual(refRes.summary.kind, "refresh");
assert.strictEqual(refRes.summary.result, "succeeded");
// pulumi destroy
const destroyRes = await stack.destroy({ userAgent });
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
});
it(`runs through the stack lifecycle with an inline program`, async () => {
const program = async () => {
const config = new Config();
return {
exp_static: "foo",
exp_cfg: config.get("bar"),
exp_secret: config.getSecret("buzz"),
};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
const stackConfig: ConfigMap = {
bar: { value: "abc" },
buzz: { value: "secret", secret: true },
};
await stack.setAllConfig(stackConfig);
// pulumi up
const upRes = await stack.up({ userAgent });
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({ userAgent });
assert.strictEqual(preRes.changeSummary.same, 1);
// pulumi refresh
const refRes = await stack.refresh({ userAgent });
assert.strictEqual(refRes.summary.kind, "refresh");
assert.strictEqual(refRes.summary.result, "succeeded");
// pulumi destroy
const destroyRes = await stack.destroy({ userAgent });
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
});
it(`runs through the stack lifecycle with an inline program, testing removing without destroying`, async () => {
const program = async () => {
class MyResource extends ComponentResource {
constructor(name: string, opts?: ComponentResourceOptions) {
super("my:module:MyResource", name, {}, opts);
}
}
new MyResource("res");
return {};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
await stack.up({ userAgent });
// we shouldn't be able to remove the stack without force
// since the stack has an active resource
assert.rejects(stack.workspace.removeStack(stackName));
await stack.workspace.removeStack(stackName, { force: true });
// we shouldn't be able to select the stack after it's been removed
// we expect this error
assert.rejects(stack.workspace.selectStack(stackName));
});
it("runs through the stack lifecycle with an inline program, testing destroy with --remove", async () => {
// Arrange.
const program = async () => {
class MyResource extends ComponentResource {
constructor(name: string, opts?: ComponentResourceOptions) {
super("my:module:MyResource", name, {}, opts);
}
}
new MyResource("res");
return {};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
await stack.up({ userAgent });
// Act.
await stack.destroy({ userAgent, remove: true });
// Assert.
await assert.rejects(stack.workspace.selectStack(stackName));
});
it(`refreshes with refresh option`, async () => {
// We create a simple program, and scan the output for an indication
// that adding refresh: true will perfrom a refresh operation.
const program = async () => {
return {
toggle: true,
};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
// • First, run Up so we can set the initial state.
await stack.up({ userAgent });
// • Next, run preview with refresh and check that the refresh was performed.
const refresh = true;
const previewRes = await stack.preview({ userAgent, refresh });
assert.match(previewRes.stdout, /refreshing/);
assert.strictEqual(previewRes.changeSummary.same, 1, "preview expected 1 same (the stack)");
const upRes = await stack.up({ userAgent, refresh });
assert.match(upRes.stdout, /refreshing/);
const destroyRes = await stack.destroy({ userAgent, refresh });
assert.match(destroyRes.stdout, /refreshing/);
});
it(`destroys an inline program with excludeProtected`, async () => {
const program = async () => {
class MyResource extends ComponentResource {
constructor(name: string, opts?: ComponentResourceOptions) {
super("my:module:MyResource", name, {}, opts);
}
}
const config = new Config();
const protect = config.getBoolean("protect") ?? false;
new MyResource("first", { protect });
new MyResource("second");
return {};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
// initial up
await stack.setConfig("protect", { value: "true" });
await stack.up({ userAgent });
// pulumi destroy
const destroyRes = await stack.destroy({ userAgent, excludeProtected: true });
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
assert.match(destroyRes.stdout, /All unprotected resources were destroyed/);
// unprotected resources
await stack.removeConfig("protect");
await stack.up({ userAgent });
// pulumi destroy to cleanup all resources
await stack.destroy({ userAgent });
await stack.workspace.removeStack(stackName);
});
it(`successfully initializes multiple stacks`, async () => {
const program = async () => {
const config = new Config();
return {
exp_static: "foo",
exp_cfg: config.get("bar"),
exp_secret: config.getSecret("buzz"),
};
};
const projectName = "inline_node";
const stackNames = Array.from(Array(10).keys()).map((_) =>
fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`),
);
const stacks = await Promise.all(
stackNames.map(
async (stackName) => LocalWorkspace.createStack({ stackName, projectName, program }),
withTestBackend({}, "inline_node"),
),
);
await stacks.map((stack) => stack.workspace.removeStack(stack.name));
});
it(`runs through the stack lifecycle with multiple inline programs in parallel`, async () => {
const program = async () => {
const config = new Config();
return {
exp_static: "foo",
exp_cfg: config.get("bar"),
exp_secret: config.getSecret("buzz"),
};
};
const projectName = "inline_node";
const stackNames = Array.from(Array(30).keys()).map((_) =>
fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`),
);
const testStackLifetime = async (stackName: string) => {
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
const stackConfig: ConfigMap = {
bar: { value: "abc" },
buzz: { value: "secret", secret: true },
};
await stack.setAllConfig(stackConfig);
// pulumi up
const upRes = await stack.up({ userAgent }); // pulumi 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({ userAgent }); // pulumi preview
assert.strictEqual(preRes.changeSummary.same, 1);
// pulumi refresh
const refRes = await stack.refresh({ userAgent });
assert.strictEqual(refRes.summary.kind, "refresh");
assert.strictEqual(refRes.summary.result, "succeeded");
// pulumi destroy
const destroyRes = await stack.destroy({ userAgent });
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stack.name);
};
for (let i = 0; i < stackNames.length; i += 10) {
const chunk = stackNames.slice(i, i + 10);
await Promise.all(chunk.map(async (stackName) => await testStackLifetime(stackName)));
}
});
it(`handles events`, async () => {
const program = async () => {
const config = new Config();
return {
exp_static: "foo",
exp_cfg: config.get("bar"),
exp_secret: config.getSecret("buzz"),
};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
const stackConfig: ConfigMap = {
bar: { value: "abc" },
buzz: { value: "secret", secret: true },
};
await stack.setAllConfig(stackConfig);
let seenSummaryEvent = false;
const findSummaryEvent = (event: EngineEvent) => {
if (event.summaryEvent) {
seenSummaryEvent = true;
}
};
// pulumi preview
const preRes = await stack.preview({ onEvent: findSummaryEvent });
assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `preview`");
assert.strictEqual(preRes.changeSummary.create, 1);
// pulumi up
seenSummaryEvent = false;
const upRes = await stack.up({ onEvent: findSummaryEvent });
assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `up`");
assert.strictEqual(upRes.summary.kind, "update");
assert.strictEqual(upRes.summary.result, "succeeded");
// pulumi preview
seenSummaryEvent = false;
const preResAgain = await stack.preview({ onEvent: findSummaryEvent });
assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `preview`");
assert.strictEqual(preResAgain.changeSummary.same, 1);
// pulumi refresh
seenSummaryEvent = false;
const refRes = await stack.refresh({ onEvent: findSummaryEvent });
assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `refresh`");
assert.strictEqual(refRes.summary.kind, "refresh");
assert.strictEqual(refRes.summary.result, "succeeded");
// pulumi destroy
seenSummaryEvent = false;
const destroyRes = await stack.destroy({ onEvent: findSummaryEvent });
assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `destroy`");
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
});
// TODO[pulumi/pulumi#7127]: Re-enabled the warning.
// Temporarily skipping test until we've re-enabled the warning.
it.skip(`has secret config warnings`, async () => {
const program = async () => {
const config = new Config();
config.get("plainstr1");
config.require("plainstr2");
config.getSecret("plainstr3");
config.requireSecret("plainstr4");
config.getBoolean("plainbool1");
config.requireBoolean("plainbool2");
config.getSecretBoolean("plainbool3");
config.requireSecretBoolean("plainbool4");
config.getNumber("plainnum1");
config.requireNumber("plainnum2");
config.getSecretNumber("plainnum3");
config.requireSecretNumber("plainnum4");
config.getObject("plainobj1");
config.requireObject("plainobj2");
config.getSecretObject("plainobj3");
config.requireSecretObject("plainobj4");
config.get("str1");
config.require("str2");
config.getSecret("str3");
config.requireSecret("str4");
config.getBoolean("bool1");
config.requireBoolean("bool2");
config.getSecretBoolean("bool3");
config.requireSecretBoolean("bool4");
config.getNumber("num1");
config.requireNumber("num2");
config.getSecretNumber("num3");
config.requireSecretNumber("num4");
config.getObject("obj1");
config.requireObject("obj2");
config.getSecretObject("obj3");
config.requireSecretObject("obj4");
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
const stackConfig: ConfigMap = {
plainstr1: { value: "1" },
plainstr2: { value: "2" },
plainstr3: { value: "3" },
plainstr4: { value: "4" },
plainbool1: { value: "true" },
plainbool2: { value: "true" },
plainbool3: { value: "true" },
plainbool4: { value: "true" },
plainnum1: { value: "1" },
plainnum2: { value: "2" },
plainnum3: { value: "3" },
plainnum4: { value: "4" },
plainobj1: { value: "{}" },
plainobj2: { value: "{}" },
plainobj3: { value: "{}" },
plainobj4: { value: "{}" },
str1: { value: "1", secret: true },
str2: { value: "2", secret: true },
str3: { value: "3", secret: true },
str4: { value: "4", secret: true },
bool1: { value: "true", secret: true },
bool2: { value: "true", secret: true },
bool3: { value: "true", secret: true },
bool4: { value: "true", secret: true },
num1: { value: "1", secret: true },
num2: { value: "2", secret: true },
num3: { value: "3", secret: true },
num4: { value: "4", secret: true },
obj1: { value: "{}", secret: true },
obj2: { value: "{}", secret: true },
obj3: { value: "{}", secret: true },
obj4: { value: "{}", secret: true },
};
await stack.setAllConfig(stackConfig);
let events: string[] = [];
const findDiagnosticEvents = (event: EngineEvent) => {
if (event.diagnosticEvent?.severity === "warning") {
events.push(event.diagnosticEvent.message);
}
};
const expectedWarnings = [
"Configuration 'inline_node:str1' value is a secret; use `getSecret` instead of `get`",
"Configuration 'inline_node:str2' value is a secret; use `requireSecret` instead of `require`",
"Configuration 'inline_node:bool1' value is a secret; use `getSecretBoolean` instead of `getBoolean`",
"Configuration 'inline_node:bool2' value is a secret; use `requireSecretBoolean` instead of `requireBoolean`",
"Configuration 'inline_node:num1' value is a secret; use `getSecretNumber` instead of `getNumber`",
"Configuration 'inline_node:num2' value is a secret; use `requireSecretNumber` instead of `requireNumber`",
"Configuration 'inline_node:obj1' value is a secret; use `getSecretObject` instead of `getObject`",
"Configuration 'inline_node:obj2' value is a secret; use `requireSecretObject` instead of `requireObject`",
];
// These keys should not be in any warning messages.
const unexpectedWarnings = [
"plainstr1",
"plainstr2",
"plainstr3",
"plainstr4",
"plainbool1",
"plainbool2",
"plainbool3",
"plainbool4",
"plainnum1",
"plainnum2",
"plainnum3",
"plainnum4",
"plainobj1",
"plainobj2",
"plainobj3",
"plainobj4",
"str3",
"str4",
"bool3",
"bool4",
"num3",
"num4",
"obj3",
"obj4",
];
const validate = (warnings: string[]) => {
for (const expected of expectedWarnings) {
let found = false;
for (const warning of warnings) {
if (warning.includes(expected)) {
found = true;
break;
}
}
assert.strictEqual(found, true, `expected warning not found`);
}
for (const unexpected of unexpectedWarnings) {
for (const warning of warnings) {
assert.strictEqual(
warning.includes(unexpected),
false,
`Unexpected '${unexpected}' found in warning`,
);
}
}
};
// pulumi preview
await stack.preview({ onEvent: findDiagnosticEvents });
validate(events);
// pulumi up
events = [];
await stack.up({ onEvent: findDiagnosticEvents });
validate(events);
await stack.workspace.removeStack(stackName);
});
it(`imports and exports stacks`, async () => {
const program = async () => {
const config = new Config();
return {
exp_static: "foo",
exp_cfg: config.get("bar"),
exp_secret: config.getSecret("buzz"),
};
};
const projectName = "import_export_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "import_export_node"),
);
try {
await stack.setAllConfig({
bar: { value: "abc" },
buzz: { value: "secret", secret: true },
});
await stack.up();
// export stack
const state = await stack.exportStack();
// import stack
await stack.importStack(state);
const configVal = await stack.getConfig("bar");
assert.strictEqual(configVal.value, "abc");
} finally {
const destroyRes = await stack.destroy();
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
}
});
// TODO[pulumi/pulumi#8061] flaky test
xit(`supports stack outputs`, async () => {
const program = async () => {
const config = new Config();
return {
exp_static: "foo",
exp_cfg: config.get("bar"),
exp_secret: config.getSecret("buzz"),
};
};
const projectName = "import_export_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "import_export_node"),
);
const assertOutputs = (outputs: OutputMap) => {
assert.strictEqual(Object.keys(outputs).length, 3, "expected to have 3 outputs");
assert.strictEqual(outputs["exp_static"].value, "foo");
assert.strictEqual(outputs["exp_static"].secret, false);
assert.strictEqual(outputs["exp_cfg"].value, "abc");
assert.strictEqual(outputs["exp_cfg"].secret, false);
assert.strictEqual(outputs["exp_secret"].value, "secret");
assert.strictEqual(outputs["exp_secret"].secret, true);
};
try {
await stack.setAllConfig({
bar: { value: "abc" },
buzz: { value: "secret", secret: true },
});
const initialOutputs = await stack.outputs();
assert.strictEqual(Object.keys(initialOutputs).length, 0, "expected initialOutputs to be empty");
// pulumi up
const upRes = await stack.up();
assert.strictEqual(upRes.summary.kind, "update");
assert.strictEqual(upRes.summary.result, "succeeded");
assertOutputs(upRes.outputs);
const outputsAfterUp = await stack.outputs();
assertOutputs(outputsAfterUp);
const destroyRes = await stack.destroy();
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
const outputsAfterDestroy = await stack.outputs();
assert.strictEqual(Object.keys(outputsAfterDestroy).length, 0, "expected outputsAfterDestroy to be empty");
} finally {
await stack.workspace.removeStack(stackName);
}
});
it(`runs an inline program that exits gracefully`, async () => {
const program = async () => ({});
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
// pulumi up
await assert.doesNotReject(stack.up());
// pulumi destroy
const destroyRes = await stack.destroy();
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
});
it(`runs an inline program that rejects a promise and exits gracefully`, async () => {
const program = async () => {
Promise.reject(new Error());
return {};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
// pulumi up
await assert.rejects(stack.up());
// pulumi destroy
const destroyRes = await stack.destroy();
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
});
it(`runs successfully after a previous failure`, async () => {
let shouldFail = true;
const program = async () => {
if (shouldFail) {
Promise.reject(new Error());
}
return {};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
// pulumi up rejects the first time
await assert.rejects(stack.up());
// pulumi up succeeds the 2nd time
shouldFail = false;
await assert.doesNotReject(stack.up());
// pulumi destroy
const destroyRes = await stack.destroy();
assert.strictEqual(destroyRes.summary.kind, "destroy");
assert.strictEqual(destroyRes.summary.result, "succeeded");
await stack.workspace.removeStack(stackName);
});
it("can import resources into a stack using resource definitions", async () => {
const workDir = upath.joinSafe(__dirname, "data", "import");
const stackName = `int_test${getTestSuffix()}`;
const stack = await LocalWorkspace.createStack({ workDir, stackName }, withTestBackend({}));
const pulumiRandomVersion = "4.16.3";
await stack.workspace.installPlugin("random", pulumiRandomVersion);
const result = await stack.import({
protect: false,
resources: [
{
type: "random:index/randomPassword:RandomPassword",
name: "randomPassword",
id: "supersecret",
},
],
});
let kind = "resource-import";
if (process.env.PULUMI_ACCESS_TOKEN) {
// The service handles this slightly differently, and we just get "update" as "kind"
kind = "update";
}
assert.strictEqual(result.summary.kind, kind);
assert.strictEqual(result.summary.result, "succeeded");
const expectedGeneratedCode = fs.readFileSync(upath.joinSafe(workDir, "expected_generated_code.txt"), "utf8");
assert.strictEqual(result.generatedCode, expectedGeneratedCode);
await stack.destroy();
await stack.workspace.removeStack(stackName);
await stack.workspace.removePlugin("random", pulumiRandomVersion);
});
it("can import resources into a stack without generating code", async () => {
const workDir = upath.joinSafe(__dirname, "data", "import");
const stackName = `int_test${getTestSuffix()}`;
const stack = await LocalWorkspace.createStack({ workDir, stackName }, withTestBackend({}));
const pulumiRandomVersion = "4.16.3";
await stack.workspace.installPlugin("random", pulumiRandomVersion);
const result = await stack.import({
protect: false,
generateCode: false,
resources: [
{
type: "random:index/randomPassword:RandomPassword",
name: "randomPassword",
id: "supersecret",
},
],
});
let kind = "resource-import";
if (process.env.PULUMI_ACCESS_TOKEN) {
// The service handles this slightly differently, and we just get "update" as "kind"
kind = "update";
}
assert.strictEqual(result.summary.kind, kind);
assert.strictEqual(result.summary.result, "succeeded");
assert.strictEqual(result.generatedCode, "");
await stack.destroy();
await stack.workspace.removeStack(stackName);
await stack.workspace.removePlugin("random", pulumiRandomVersion);
});
it(`sets pulumi version`, async () => {
const ws = await LocalWorkspace.create(withTestBackend({}));
assert(ws.pulumiVersion);
assert.strictEqual(versionRegex.test(ws.pulumiVersion), true);
});
it("sets pulumi version when using a custom CLI instance", async () => {
const tmpDir = tmp.dirSync({ prefix: "automation-test-", unsafeCleanup: true });
try {
const cmd = await PulumiCommand.get();
const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: cmd }));
assert.strictEqual(versionRegex.test(ws.pulumiVersion), true);
} finally {
tmpDir.removeCallback();
}
});
it("throws when attempting to retrieve an invalid pulumi version", async () => {
const mockWithNoVersion = {
command: "pulumi",
version: null,
run: async () => new CommandResult("some output", "", 0),
};
const ws = await LocalWorkspace.create(
withTestBackend({
pulumiCommand: mockWithNoVersion,
envVars: {
PULUMI_AUTOMATION_API_SKIP_VERSION_CHECK: "true",
},
}),
);
assert.throws(() => ws.pulumiVersion);
});
it("fails creation if remote operation is not supported", async () => {
const mockWithNoRemoteSupport = {
command: "pulumi",
version: new semver.SemVer("2.0.0"),
// We inspect the output of `pulumi preview --help` to determine
// if the CLI supports remote operations, see
// `LocalWorkspace.checkRemoteSupport`.
run: async () => new CommandResult("some output", "", 0),
};
await assert.rejects(
LocalWorkspace.create(withTestBackend({ pulumiCommand: mockWithNoRemoteSupport, remote: true })),
);
});
it("bypasses remote support check", async () => {
const mockWithNoRemoteSupport = {
command: "pulumi",
version: new semver.SemVer("2.0.0"),
// We inspect the output of `pulumi preview --help` to determine
// if the CLI supports remote operations, see
// `LocalWorkspace.checkRemoteSupport`.
run: async () => new CommandResult("some output", "", 0),
};
await assert.doesNotReject(
LocalWorkspace.create(
withTestBackend({
pulumiCommand: mockWithNoRemoteSupport,
remote: true,
envVars: {
PULUMI_AUTOMATION_API_SKIP_VERSION_CHECK: "true",
},
}),
),
);
});
it(`respects existing project settings`, async () => {
const projectName = "correct_project";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{
stackName,
projectName,
program: async () => {
return;
},
},
withTestBackend(
{ workDir: upath.joinSafe(__dirname, "data", "correct_project") },
"correct_project",
"This is a description",
),
);
const projectSettings = await stack.workspace.projectSettings();
assert.strictEqual(projectSettings.name, "correct_project");
// the description check is enough to verify that the stack wasn't overwritten
assert.strictEqual(projectSettings.description, "This is a description");
await stack.workspace.removeStack(stackName);
});
it(`correctly sets config on multiple stacks concurrently`, async () => {
const dones = [];
const stacks = ["dev", "dev2", "dev3", "dev4", "dev5"];
const workDir = upath.joinSafe(__dirname, "data", "tcfg");
const ws = await LocalWorkspace.create({
workDir,
projectSettings: {
name: "concurrent-config",
runtime: "nodejs",
backend: { url: "file://~" },
},
envVars: {
PULUMI_CONFIG_PASSPHRASE: "test",
},
});
for (let i = 0; i < stacks.length; i++) {
await Stack.create(stacks[i], ws);
}
for (let i = 0; i < stacks.length; i++) {
const x = i;
const s = stacks[i];
dones.push(
(async () => {
for (let j = 0; j < 20; j++) {
await ws.setConfig(s, "var-" + j, { value: (x * 20 + j).toString() });
}
})(),
);
}
await Promise.all(dones);
for (let i = 0; i < stacks.length; i++) {
const stack = await LocalWorkspace.selectStack({
stackName: stacks[i],
workDir,
});
const config = await stack.getAllConfig();
assert.strictEqual(Object.keys(config).length, 20);
await stack.workspace.removeStack(stacks[i]);
}
});
it(`runs the install command`, async () => {
let recordedArgs: string[] = [];
const mockCommand = {
command: "pulumi",
// Version high enough to support --use-language-version-tools
version: semver.parse("3.130.0"),
run: async (
args: string[],
cwd: string,
additionalEnv: { [key: string]: string },
onOutput?: (data: string) => void,
): Promise<CommandResult> => {
recordedArgs = args;
return new CommandResult("some output", "", 0);
},
};
const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: mockCommand }));
await ws.install();
assert.deepStrictEqual(recordedArgs, ["install"]);
await ws.install({ noPlugins: true });
assert.deepStrictEqual(recordedArgs, ["install", "--no-plugins"]);
await ws.install({ noDependencies: true });
assert.deepStrictEqual(recordedArgs, ["install", "--no-dependencies"]);
await ws.install({ reinstall: true });
assert.deepStrictEqual(recordedArgs, ["install", "--reinstall"]);
await ws.install({ useLanguageVersionTools: true });
assert.deepStrictEqual(recordedArgs, ["install", "--use-language-version-tools"]);
await ws.install({
noDependencies: true,
noPlugins: true,
reinstall: true,
useLanguageVersionTools: true,
});
assert.deepStrictEqual(recordedArgs, [
"install",
"--use-language-version-tools",
"--no-plugins",
"--no-dependencies",
"--reinstall",
]);
});
it(`install requires version >= 3.91`, async () => {
const mockCommand = {
command: "pulumi",
version: semver.parse("3.90.0"),
run: async (
args: string[],
cwd: string,
additionalEnv: { [key: string]: string },
onOutput?: (data: string) => void,
): Promise<CommandResult> => {
return new CommandResult("some output", "", 0);
},
};
const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: mockCommand }));
assert.rejects(() => ws.install());
});
it(`install --use-language-version-tools requires version >= 3.130`, async () => {
const mockCommand = {
command: "pulumi",
version: semver.parse("3.129.0"),
run: async (
args: string[],
cwd: string,
additionalEnv: { [key: string]: string },
onOutput?: (data: string) => void,
): Promise<CommandResult> => {
return new CommandResult("some output", "", 0);
},
};
const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: mockCommand }));
assert.rejects(() => ws.install());
});
it("sends SIGINT when aborted", async () => {
const controller = new AbortController();
const program = async () => {
await new Promise((f) => setTimeout(f, 60000));
return {};
};
const projectName = "inline_node";
const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
const stack = await LocalWorkspace.createStack(
{ stackName, projectName, program },
withTestBackend({}, "inline_node"),
);
new Promise((f) => setTimeout(f, 500)).then(() => controller.abort());
try {
// pulumi preview
const previewRes = await stack.preview({
signal: controller.signal,
});
assert.fail("expected canceled preview to throw");
} catch (err) {
assert.match(err.toString(), /stderr: Command was killed with SIGINT|error: preview canceled/);
assert.match(err.toString(), /CommandError: code: -2/);
}
await stack.workspace.removeStack(stackName);
});
});
const normalizeConfigKey = (key: string, projectName: string) => {
const parts = key.split(":");
if (parts.length < 2) {
return `${projectName}:${key}`;
}
return "";
};
/**
* Augments the provided {@link LocalWorkspaceOptions} so that they reference a
* either a file backend or a cloud backend, depending on whether PULUMI_ACCESS_TOKEN
* is set in the environment.
*/
function withTestBackend(
opts?: LocalWorkspaceOptions,
name?: string,
description?: string,
runtime?: string,
): LocalWorkspaceOptions {
if (process.env.PULUMI_ACCESS_TOKEN) {
return withCloudBackend(opts, name, description, runtime);
}
return withTemporaryFileBackend(opts, name, description, runtime);
}
function withCloudBackend(
opts?: LocalWorkspaceOptions,
name?: string,
description?: string,
runtime?: string,
): LocalWorkspaceOptions {
const backend = {
url: "https://api.pulumi.com",
};
if (name === undefined) {
name = "node_test";
}
if (runtime === undefined) {
runtime = "nodejs";
}
return {
...opts,
projectSettings: {
// We are obliged to provide a name and runtime if we provide project
// settings, so we do so, but we spread in the provided project settings
// afterwards so that the caller can override them if need be.
name: name,
runtime: runtime as ProjectRuntime,
description: description,
...opts?.projectSettings,
backend,
},
};
}
function withTemporaryFileBackend(
opts?: LocalWorkspaceOptions,
name?: string,
description?: string,
runtime?: string,
): LocalWorkspaceOptions {
const tmpDir = tmp.dirSync({
prefix: "nodejs-tests-automation-",
unsafeCleanup: true,
});
const backend = { url: `file://${tmpDir.name}` };
if (name === undefined) {
name = "node_test";
}
if (runtime === undefined) {
runtime = "nodejs";
}
return withTestConfigPassphrase({
...opts,
pulumiHome: tmpDir.name,
projectSettings: {
// We are obliged to provide a name and runtime if we provide project
// settings, so we do so, but we spread in the provided project settings
// afterwards so that the caller can override them if need be.
name: name,
runtime: runtime as ProjectRuntime,
description: description,
...opts?.projectSettings,
backend,
},
});
}
/**
* Augments the provided {@link LocalWorkspaceOptions} so that they set up an
* environment containing a test `PULUMI_CONFIG_PASSPHRASE` variable suitable
* for use with a local file backend.
*/
function withTestConfigPassphrase(opts?: LocalWorkspaceOptions): LocalWorkspaceOptions {
return {
...opts,
envVars: {
...opts?.envVars,
PULUMI_CONFIG_PASSPHRASE: "test",
},
};
}