mirror of https://github.com/pulumi/pulumi.git
1697 lines
69 KiB
TypeScript
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",
|
|
},
|
|
};
|
|
}
|