pulumi/tests/integration/integration_test.go

1262 lines
41 KiB
Go

// Copyright 2016-2023, 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.
//go:build all
package ints
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pulumi/pulumi/pkg/v3/codegen/testing/test"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/v3/testing/integration"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
"github.com/pulumi/pulumi/sdk/v3/python/toolchain"
)
// TestStackTagValidation verifies various error scenarios related to stack names and tags.
func TestStackTagValidation(t *testing.T) {
t.Parallel()
t.Run("Error_StackName", func(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.RunCommand("git", "init")
e.ImportDirectory("stack_project_name")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
stdout, stderr := e.RunCommandExpectError("pulumi", "stack", "init", "invalid name (spaces, parens, etc.)")
assert.Equal(t, "", stdout)
assert.Contains(t, stderr,
"a stack name may only contain alphanumeric, hyphens, underscores, or periods")
})
t.Run("Error_DescriptionLength", func(t *testing.T) {
t.Parallel()
// This test requires the service, as only the service supports stack tags.
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
}
if os.Getenv("PULUMI_TEST_OWNER") == "" {
t.Skipf("Skipping: PULUMI_TEST_OWNER is not set")
}
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
stackName, err := resource.NewUniqueHex("test-", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
e.RunCommand("git", "init")
e.ImportDirectory("stack_project_name")
prefix := "lorem ipsum dolor sit amet" // 26
prefix = prefix + prefix + prefix + prefix // 104
prefix = prefix + prefix + prefix + prefix // 416 + the current Pulumi.yaml's description
// Change the contents of the Description property of Pulumi.yaml.
yamlPath := filepath.Join(e.CWD, "Pulumi.yaml")
err = integration.ReplaceInFile("description: ", "description: "+prefix, yamlPath)
assert.NoError(t, err)
stdout, stderr := e.RunCommandExpectError("pulumi", "stack", "init", stackName)
assert.Equal(t, "", stdout)
assert.Contains(t, stderr, "error: could not create stack:")
assert.Contains(t, stderr, "validating stack properties:")
assert.Contains(t, stderr, "stack tag \"pulumi:description\" value is too long (max length 256 characters)")
})
}
// TestStackInitValidation verifies various error scenarios related to init'ing a stack.
func TestStackInitValidation(t *testing.T) {
t.Parallel()
t.Run("Error_InvalidStackYaml", func(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.RunCommand("git", "init")
e.ImportDirectory("stack_project_name")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
// Starting a yaml value with a quote string and then more data is invalid
invalidYaml := "\"this is invalid\" yaml because of trailing data after quote string"
// Change the contents of the Description property of Pulumi.yaml.
yamlPath := filepath.Join(e.CWD, "Pulumi.yaml")
err := integration.ReplaceInFile("description: ", "description: "+invalidYaml, yamlPath)
assert.NoError(t, err)
stdout, stderr := e.RunCommandExpectError("pulumi", "stack", "init", "valid-name")
assert.Equal(t, "", stdout)
assert.Contains(t, stderr, "invalid YAML file")
})
}
// TestConfigPaths ensures that config commands with paths work as expected.
func TestConfigPaths(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
// Initialize an empty stack.
path := filepath.Join(e.RootPath, "Pulumi.yaml")
err := (&workspace.Project{
Name: "testing-config",
Runtime: workspace.NewProjectRuntimeInfo("nodejs", nil),
}).Save(path)
assert.NoError(t, err)
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
e.RunCommand("pulumi", "stack", "init", "testing")
namespaces := []string{"", "my:"}
tests := []struct {
Key string
Value string
Secret bool
Path bool
TopLevelKey string
TopLevelExpectedValue string
}{
{
Key: "aConfigValue",
Value: "this value is a value",
TopLevelKey: "aConfigValue",
TopLevelExpectedValue: "this value is a value",
},
{
Key: "anotherConfigValue",
Value: "this value is another value",
TopLevelKey: "anotherConfigValue",
TopLevelExpectedValue: "this value is another value",
},
{
Key: "bEncryptedSecret",
Value: "this super secret is encrypted",
Secret: true,
TopLevelKey: "bEncryptedSecret",
TopLevelExpectedValue: "this super secret is encrypted",
},
{
Key: "anotherEncryptedSecret",
Value: "another encrypted secret",
Secret: true,
TopLevelKey: "anotherEncryptedSecret",
TopLevelExpectedValue: "another encrypted secret",
},
{
Key: "[]",
Value: "square brackets value",
TopLevelKey: "[]",
TopLevelExpectedValue: "square brackets value",
},
{
Key: "x.y",
Value: "x.y value",
TopLevelKey: "x.y",
TopLevelExpectedValue: "x.y value",
},
{
Key: "0",
Value: "0 value",
Path: true,
TopLevelKey: "0",
TopLevelExpectedValue: "0 value",
},
{
Key: "true",
Value: "value",
Path: true,
TopLevelKey: "true",
TopLevelExpectedValue: "value",
},
{
Key: `["test.Key"]`,
Value: "test key value",
Path: true,
TopLevelKey: "test.Key",
TopLevelExpectedValue: "test key value",
},
{
Key: `nested["test.Key"]`,
Value: "nested test key value",
Path: true,
TopLevelKey: "nested",
TopLevelExpectedValue: `{"test.Key":"nested test key value"}`,
},
{
Key: "outer.inner",
Value: "value",
Path: true,
TopLevelKey: "outer",
TopLevelExpectedValue: `{"inner":"value"}`,
},
{
Key: "names[0]",
Value: "a",
Path: true,
TopLevelKey: "names",
TopLevelExpectedValue: `["a"]`,
},
{
Key: "names[1]",
Value: "b",
Path: true,
TopLevelKey: "names",
TopLevelExpectedValue: `["a","b"]`,
},
{
Key: "names[2]",
Value: "c",
Path: true,
TopLevelKey: "names",
TopLevelExpectedValue: `["a","b","c"]`,
},
{
Key: "names[3]",
Value: "super secret name",
Path: true,
Secret: true,
TopLevelKey: "names",
TopLevelExpectedValue: `["a","b","c","super secret name"]`,
},
{
Key: "servers[0].port",
Value: "80",
Path: true,
TopLevelKey: "servers",
TopLevelExpectedValue: `[{"port":80}]`,
},
{
Key: "servers[0].host",
Value: "example",
Path: true,
TopLevelKey: "servers",
TopLevelExpectedValue: `[{"host":"example","port":80}]`,
},
{
Key: "a.b[0].c",
Value: "true",
Path: true,
TopLevelKey: "a",
TopLevelExpectedValue: `{"b":[{"c":true}]}`,
},
{
Key: "a.b[1].c",
Value: "false",
Path: true,
TopLevelKey: "a",
TopLevelExpectedValue: `{"b":[{"c":true},{"c":false}]}`,
},
{
Key: "tokens[0]",
Value: "shh",
Path: true,
Secret: true,
TopLevelKey: "tokens",
TopLevelExpectedValue: `["shh"]`,
},
{
Key: "foo.bar",
Value: "don't tell",
Path: true,
Secret: true,
TopLevelKey: "foo",
TopLevelExpectedValue: `{"bar":"don't tell"}`,
},
{
Key: "semiInner.a.b.c.d",
Value: "1",
Path: true,
TopLevelKey: "semiInner",
TopLevelExpectedValue: `{"a":{"b":{"c":{"d":1}}}}`,
},
{
Key: "wayInner.a.b.c.d.e.f.g.h.i.j.k",
Value: "false",
Path: true,
TopLevelKey: "wayInner",
TopLevelExpectedValue: `{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"k":false}}}}}}}}}}}`,
},
{
Key: "foo1[0]",
Value: "false",
Path: true,
TopLevelKey: "foo1",
TopLevelExpectedValue: `[false]`,
},
{
Key: "foo2[0]",
Value: "true",
Path: true,
TopLevelKey: "foo2",
TopLevelExpectedValue: `[true]`,
},
{
Key: "foo3[0]",
Value: "10",
Path: true,
TopLevelKey: "foo3",
TopLevelExpectedValue: `[10]`,
},
{
Key: "foo4[0]",
Value: "0",
Path: true,
TopLevelKey: "foo4",
TopLevelExpectedValue: `[0]`,
},
{
Key: "foo5[0]",
Value: "00",
Path: true,
TopLevelKey: "foo5",
TopLevelExpectedValue: `["00"]`,
},
{
Key: "foo6[0]",
Value: "01",
Path: true,
TopLevelKey: "foo6",
TopLevelExpectedValue: `["01"]`,
},
{
Key: "foo7[0]",
Value: "0123456",
Path: true,
TopLevelKey: "foo7",
TopLevelExpectedValue: `["0123456"]`,
},
{
Key: "bar1.inner",
Value: "false",
Path: true,
TopLevelKey: "bar1",
TopLevelExpectedValue: `{"inner":false}`,
},
{
Key: "bar2.inner",
Value: "true",
Path: true,
TopLevelKey: "bar2",
TopLevelExpectedValue: `{"inner":true}`,
},
{
Key: "bar3.inner",
Value: "10",
Path: true,
TopLevelKey: "bar3",
TopLevelExpectedValue: `{"inner":10}`,
},
{
Key: "bar4.inner",
Value: "0",
Path: true,
TopLevelKey: "bar4",
TopLevelExpectedValue: `{"inner":0}`,
},
{
Key: "bar5.inner",
Value: "00",
Path: true,
TopLevelKey: "bar5",
TopLevelExpectedValue: `{"inner":"00"}`,
},
{
Key: "bar6.inner",
Value: "01",
Path: true,
TopLevelKey: "bar6",
TopLevelExpectedValue: `{"inner":"01"}`,
},
{
Key: "bar7.inner",
Value: "0123456",
Path: true,
TopLevelKey: "bar7",
TopLevelExpectedValue: `{"inner":"0123456"}`,
},
// Overwriting a top-level string value is allowed.
{
Key: "aConfigValue.inner",
Value: "new value",
Path: true,
TopLevelKey: "aConfigValue",
TopLevelExpectedValue: `{"inner":"new value"}`,
},
{
Key: "anotherConfigValue[0]",
Value: "new value",
Path: true,
TopLevelKey: "anotherConfigValue",
TopLevelExpectedValue: `["new value"]`,
},
{
Key: "bEncryptedSecret.inner",
Value: "new value",
Path: true,
TopLevelKey: "bEncryptedSecret",
TopLevelExpectedValue: `{"inner":"new value"}`,
},
{
Key: "anotherEncryptedSecret[0]",
Value: "new value",
Path: true,
TopLevelKey: "anotherEncryptedSecret",
TopLevelExpectedValue: `["new value"]`,
},
}
validateConfigGet := func(key string, value string, path bool) {
args := []string{"config", "get", key}
if path {
args = append(args, "--path")
}
stdout, stderr := e.RunCommand("pulumi", args...)
assert.Equal(t, value+"\n", stdout)
assert.Equal(t, "", stderr)
}
for _, ns := range namespaces {
for _, test := range tests {
key := fmt.Sprintf("%s%s", ns, test.Key)
topLevelKey := fmt.Sprintf("%s%s", ns, test.TopLevelKey)
// Set the value.
args := []string{"config", "set"}
if test.Secret {
args = append(args, "--secret")
}
if test.Path {
args = append(args, "--path")
}
args = append(args, key, test.Value)
stdout, stderr := e.RunCommand("pulumi", args...)
assert.Equal(t, "", stdout)
assert.Equal(t, "", stderr)
// Get the value and validate it.
validateConfigGet(key, test.Value, test.Path)
// Get the top-level value and validate it.
validateConfigGet(topLevelKey, test.TopLevelExpectedValue, false /*path*/)
}
}
badKeys := []string{
// Syntax errors.
"root[",
`root["nested]`,
"root.array[abc]",
// First path segment must be a non-empty string.
`[""]`,
"[0]",
".foo",
".[0]",
// Index out of range.
"names[-1]",
"names[5]",
// A "secure" key that is a map with a single string value is reserved by the system.
"key.secure",
"super.nested.map.secure",
// Type mismatch.
"outer[0]",
"names.nested",
"outer.inner.nested",
"outer.inner[0]",
}
for _, ns := range namespaces {
for _, badKey := range badKeys {
key := fmt.Sprintf("%s%s", ns, badKey)
stdout, stderr := e.RunCommandExpectError("pulumi", "config", "set", "--path", key, "value")
assert.Equal(t, "", stdout)
assert.NotEqual(t, "", stderr)
}
}
e.RunCommand("pulumi", "stack", "rm", "--yes")
}
func testDestroyStackRef(e *ptesting.Environment, organization string) {
e.ImportDirectory("large_resource/nodejs")
stackName, err := resource.NewUniqueHex("rm-test-", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
e.RunCommand("pulumi", "stack", "init", stackName)
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("yarn", "install")
e.RunCommand("pulumi", "up", "--skip-preview", "--yes")
e.CWD = os.TempDir()
stackRef := stackName
if organization != "" {
stackRef = organization + "/large_resource_js/" + stackName
}
e.RunCommand("pulumi", "destroy", "--skip-preview", "--yes", "-s", stackRef)
e.RunCommand("pulumi", "stack", "rm", "--yes", "-s", stackRef)
}
//nolint:paralleltest // uses parallel programtest
func TestDestroyStackRef_LocalProject(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
testDestroyStackRef(e, "organization")
}
//nolint:paralleltest // uses parallel programtest
func TestDestroyStackRef_LocalNonProject_NewEnv(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
t.Setenv("PULUMI_DIY_BACKEND_LEGACY_LAYOUT", "true")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
testDestroyStackRef(e, "")
}
//nolint:paralleltest // uses parallel programtest
func TestDestroyStackRef_LocalNonProject_OldEnv(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
t.Setenv("PULUMI_SELF_MANAGED_STATE_LEGACY_LAYOUT", "true")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
testDestroyStackRef(e, "")
}
//nolint:paralleltest // uses parallel programtest
func TestDestroyStackRef_Cloud(t *testing.T) {
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
}
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
output, _ := e.RunCommand("pulumi", "whoami")
organization := strings.TrimSpace(output)
testDestroyStackRef(e, organization)
}
//nolint:paralleltest // uses parallel programtest
func TestJSONOutput(t *testing.T) {
stdout := &bytes.Buffer{}
// Test without env var for streaming preview (should print previewSummary).
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: filepath.Join("stack_outputs", "nodejs"),
Dependencies: []string{"@pulumi/pulumi"},
Stdout: stdout,
Verbose: true,
JSONOutput: true,
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
output := stdout.String()
// Check that the previewSummary is present.
assert.Regexp(t, previewSummaryRegex, output)
// Check that each event present in the event stream is also in stdout.
for _, evt := range stack.Events {
assertOutputContainsEvent(t, evt, output)
}
},
})
}
func TestProviderDownloadURL(t *testing.T) {
t.Parallel()
validate := func(t *testing.T, stdout []byte) {
deployment := &apitype.UntypedDeployment{}
err := json.Unmarshal(stdout, deployment)
assert.NoError(t, err)
data := &apitype.DeploymentV3{}
err = json.Unmarshal(deployment.Deployment, data)
assert.NoError(t, err)
urlKey := "pluginDownloadURL"
getPluginDownloadURL := func(inputs map[string]interface{}) string {
internal, ok := inputs["__internal"].(map[string]interface{})
if ok {
pluginDownloadURL, _ := internal[urlKey].(string)
return pluginDownloadURL
}
return ""
}
for _, resource := range data.Resources {
switch {
case providers.IsDefaultProvider(resource.URN):
assert.Equalf(t, "get.example.test", getPluginDownloadURL(resource.Inputs), "Inputs")
assert.Equalf(t, "", getPluginDownloadURL(resource.Outputs), "Outputs")
assert.Equalf(t, nil, resource.Inputs[urlKey], "Outputs")
assert.Equalf(t, nil, resource.Outputs[urlKey], "Outputs")
case providers.IsProviderType(resource.Type):
assert.Equalf(t, "get.pulumi.test/providers", getPluginDownloadURL(resource.Inputs), "Inputs")
assert.Equalf(t, "", getPluginDownloadURL(resource.Outputs), "Outputs")
assert.Equalf(t, nil, resource.Inputs[urlKey], "Outputs")
assert.Equalf(t, nil, resource.Outputs[urlKey], "Outputs")
default:
_, hasURL := resource.Inputs[urlKey]
assert.False(t, hasURL)
_, hasURL = resource.Outputs[urlKey]
assert.False(t, hasURL)
}
}
assert.Greater(t, len(data.Resources), 1, "We should construct more then just the stack")
}
languages := []struct {
name string
dependency string
}{
{"python", filepath.Join("..", "..", "sdk", "python", "env", "src")},
{"nodejs", "@pulumi/pulumi"},
{"go", "github.com/pulumi/pulumi/sdk/v3"},
}
//nolint:paralleltest // uses parallel programtest
for _, lang := range languages {
lang := lang
t.Run(lang.name, func(t *testing.T) {
dir := filepath.Join("gather_plugin", lang.name)
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: dir,
ExportStateValidator: validate,
SkipPreview: true,
SkipEmptyPreviewUpdate: true,
Dependencies: []string{lang.dependency},
})
})
}
}
func TestExcludeProtected(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.ImportDirectory("exclude_protected")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
e.RunCommand("pulumi", "stack", "init", "dev")
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("yarn", "install")
e.RunCommand("pulumi", "up", "--skip-preview", "--yes")
stdout, _ := e.RunCommand("pulumi", "destroy", "--skip-preview", "--yes", "--exclude-protected")
assert.Contains(t, stdout, "All unprotected resources were destroyed. There are still 7 protected resources")
// We run the command again, but this time there are not unprotected resources to destroy.
stdout, _ = e.RunCommand("pulumi", "destroy", "--skip-preview", "--yes", "--exclude-protected")
assert.Contains(t, stdout, "There were no unprotected resources to destroy. There are still 7")
}
func TestInvalidPluginError(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
pulumiProject := `
name: invalid-plugin
runtime: yaml
description: A Pulumi program referencing an invalid plugin.
plugins:
providers:
- name: fakeplugin
bin: ./does/not/exist/bin # key should be 'path'
`
integration.CreatePulumiRepo(e, pulumiProject)
e.SetBackend(e.LocalURL())
{
_, stderr := e.RunCommandExpectError("pulumi", "stack", "init", "invalid-resources")
assert.NotContains(t, stderr, "panic: ")
assert.Contains(t, stderr, "error: ")
}
{
_, stderr := e.RunCommandExpectError("pulumi", "pre")
assert.NotContains(t, stderr, "panic: ")
assert.Contains(t, stderr, "error: ")
}
}
// Regression test for https://github.com/pulumi/pulumi/issues/12632.
func TestPassphraseSetAllGet(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
e.Passphrase = "test-passphrase"
defer e.DeleteIfNotFailed()
pulumiProject := `
name: passphrase-test
runtime: yaml
description: A Pulumi program testing passphrase config.
`
integration.CreatePulumiRepo(e, pulumiProject)
e.SetBackend(e.LocalURL())
// Init a new stack, then set a secret config value, then try to get it.
e.RunCommand("pulumi", "stack", "init", "passphrase-test")
// Clear the config file so that "config set-all" has to re-initialize the passphrase config.
err := os.Remove(filepath.Join(e.RootPath, "Pulumi.passphrase-test.yaml"))
require.NoError(t, err)
// Set a secret config value, then try to get it.
e.RunCommand("pulumi", "config", "set-all", "--secret", "foo=bar")
stdout, _ := e.RunCommand("pulumi", "config", "get", "foo")
assert.Contains(t, stdout, "bar")
}
// Similar to TestPassphraseSetAllGet but covering for "set" instead of "set-all".
func TestPassphraseSetGet(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
e.Passphrase = "test-passphrase"
defer e.DeleteIfNotFailed()
pulumiProject := `
name: passphrase-test
runtime: yaml
description: A Pulumi program testing passphrase config.
`
integration.CreatePulumiRepo(e, pulumiProject)
e.SetBackend(e.LocalURL())
// Init a new stack, then set a secret config value, then try to get it.
e.RunCommand("pulumi", "stack", "init", "passphrase-test")
// Clear the config file so that "config set" has to re-initialize the passphrase config.
err := os.Remove(filepath.Join(e.RootPath, "Pulumi.passphrase-test.yaml"))
require.NoError(t, err)
// Set a secret config value, then try to get it.
e.RunCommand("pulumi", "config", "set", "--secret", "foo", "bar")
stdout, _ := e.RunCommand("pulumi", "config", "get", "foo")
assert.Contains(t, stdout, "bar")
}
// Regression test for https://github.com/pulumi/pulumi/issues/12593.
//
// Verifies that a "provider" option passed to a remote component
// is properly propagated to the component's children.
//
// Language-specific tests should call this function with the
// appropriate parameters.
func testConstructProviderPropagation(t *testing.T, lang string, deps []string) {
const (
testDir = "construct_component_provider_propagation"
componentDir = "testcomponent-go"
)
runComponentSetup(t, testDir)
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: filepath.Join(testDir, lang),
Dependencies: deps,
LocalProviders: []integration.LocalDependency{
{
Package: "testcomponent",
Path: filepath.Join(testDir, componentDir),
},
},
Quick: true,
NoParallel: true, // already called by tests
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
gotProviders := make(map[string]string) // resource name => provider name
for _, res := range stackInfo.Deployment.Resources {
if res.URN.Type() == "testprovider:index:Random" {
ref, err := providers.ParseReference(res.Provider)
assert.NoError(t, err)
if err == nil {
gotProviders[res.URN.Name()] = ref.URN().Name()
}
}
}
assert.Equal(t, map[string]string{
"uses_default": "default",
"uses_provider": "explicit",
"uses_providers": "explicit",
"uses_providers_map": "explicit",
}, gotProviders)
},
})
}
// Test to validate that various resource options are propagated for MLCs.
func testConstructResourceOptions(t *testing.T, dir string, deps []string) {
const (
testDir = "construct_component_resource_options"
componentDir = "testcomponent-go"
)
runComponentSetup(t, testDir)
validate := func(t *testing.T, resources []apitype.ResourceV3) {
urns := make(map[string]resource.URN) // name => URN
for _, res := range resources {
urns[res.URN.Name()] = res.URN
}
for _, res := range resources {
switch name := res.URN.Name(); name {
case "Protect":
assert.True(t, res.Protect, "Protect(%s)", name)
case "DependsOn":
wantDeps := []resource.URN{urns["Dep1"], urns["Dep2"]}
assert.ElementsMatch(t, wantDeps, res.Dependencies,
"DependsOn(%s)", name)
case "AdditionalSecretOutputs":
assert.Equal(t,
[]resource.PropertyKey{"foo"}, res.AdditionalSecretOutputs,
"AdditionalSecretOutputs(%s)", name)
case "CustomTimeouts":
if ct := res.CustomTimeouts; assert.NotNil(t, ct, "CustomTimeouts(%s)", name) {
assert.Equal(t, float64(60), ct.Create, "CustomTimeouts.Create(%s)", name)
assert.Equal(t, float64(120), ct.Update, "CustomTimeouts.Update(%s)", name)
assert.Equal(t, float64(180), ct.Delete, "CustomTimeouts.Delete(%s)", name)
}
case "DeletedWith":
assert.Equal(t, urns["getDeletedWithMe"], res.DeletedWith, "DeletedWith(%s)", name)
case "RetainOnDelete":
assert.True(t, res.RetainOnDelete, "RetainOnDelete(%s)", name)
}
}
}
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: filepath.Join(testDir, dir),
Dependencies: deps,
LocalProviders: []integration.LocalDependency{
{
Package: "testcomponent",
Path: filepath.Join(testDir, componentDir),
},
},
Quick: true,
NoParallel: true, // already called by tests
DestroyExcludeProtected: true, // test contains protected resources
SkipStackRemoval: true, // protected resources prevent stack removal
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
validate(t, stackInfo.Deployment.Resources)
},
})
}
func testProjectRename(e *ptesting.Environment, organization string) {
e.ImportDirectory("large_resource/nodejs")
stackName, err := resource.NewUniqueHex("rm-test-", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
e.RunCommand("pulumi", "stack", "init", stackName)
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("yarn", "install")
e.RunCommand("pulumi", "up", "--skip-preview", "--yes")
newProjectName := "new_large_resource_js"
stackRef := organization + "/" + newProjectName + "/" + stackName
e.RunCommand("pulumi", "stack", "rename", stackRef)
// Rename the project name in the yaml file
projFilename := filepath.Join(e.CWD, "Pulumi.yaml")
proj, err := workspace.LoadProject(projFilename)
require.NoError(e, err)
proj.Name = tokens.PackageName(newProjectName)
err = proj.Save(projFilename)
require.NoError(e, err)
e.RunCommand("pulumi", "up", "--skip-preview", "--yes", "--expect-no-changes", "-s", stackRef)
e.RunCommand("pulumi", "stack", "rm", "--force", "--yes", "-s", stackRef)
}
//nolint:paralleltest // uses parallel programtest
func TestProjectRename_LocalProject(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
testProjectRename(e, "organization")
}
//nolint:paralleltest // uses parallel programtest
func TestProjectRename_Cloud(t *testing.T) {
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
}
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
output, _ := e.RunCommand("pulumi", "whoami")
organization := strings.TrimSpace(output)
testProjectRename(e, organization)
}
//nolint:paralleltest // uses parallel programtest
func TestParentRename_issue13179(t *testing.T) {
// This test is a reproduction of the issue reported in
// https://github.com/pulumi/pulumi/issues/13179.
//
// It creates a stack with a resource that has a parent
// and then renames the parent resource with 'pulumi state rename'.
var parentURN resource.URN
pt := integration.ProgramTestManualLifeCycle(t, &integration.ProgramTestOptions{
Dir: "state_rename_parent",
Dependencies: []string{
"github.com/pulumi/pulumi/sdk/v3",
},
LocalProviders: []integration.LocalDependency{
{Package: "testprovider", Path: filepath.Join("..", "testprovider")},
},
// Only run up:
SkipRefresh: true,
Quick: true,
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
for _, res := range stackInfo.Deployment.Resources {
if res.URN.Name() == "parent" {
parentURN = res.URN
}
}
},
})
require.NoError(t, pt.TestLifeCyclePrepare(), "prepare")
t.Cleanup(pt.TestCleanUp)
require.NoError(t, pt.TestLifeCycleInitialize(), "initialize")
require.NoError(t, pt.TestPreviewUpdateAndEdits(), "update")
// PreviewUpdateAndEdits calls ExtraRuntimeValidation,
// so we should have captured the parent URN.
require.NotEmpty(t, parentURN, "no parent URN captured")
// Rename the parent resource.
require.NoError(t,
pt.RunPulumiCommand("state", "rename", "-y", string(parentURN), "newParent"),
"rename failed")
}
func testStackRmConfig(e *ptesting.Environment, organization string) {
// We need to create two projects for this test
goDir := filepath.Join(e.RootPath, "large_resource_go")
err := os.Mkdir(goDir, 0o700)
require.NoError(e, err)
jsDir := filepath.Join(e.RootPath, "large_resource_js")
err = os.Mkdir(jsDir, 0o700)
require.NoError(e, err)
stackName, err := resource.NewUniqueHex("rm-test-", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
// Create a stack in the go project
e.CWD = goDir
e.ImportDirectory("large_resource/go")
e.RunCommand("pulumi", "stack", "init", stackName)
// Create a config value to ensure there's a Pulumi.<name>.yaml file.
e.RunCommand("pulumi", "config", "set", "key", "value")
// Now create the js project
e.CWD = jsDir
e.ImportDirectory("large_resource/nodejs")
e.RunCommand("pulumi", "stack", "init", stackName)
// Create a config value to ensure there's a Pulumi.<name>.yaml file.
e.RunCommand("pulumi", "config", "set", "key", "value")
// Now try and remove the go stack while still in the js directory
stackRef := organization + "/large_resource_go/" + stackName
e.RunCommand("pulumi", "stack", "rm", "--yes", "-s", stackRef)
// And check that Pulumi.<name>.yaml file is still there for the js project
_, err = os.Stat(filepath.Join(jsDir, "Pulumi."+stackName+".yaml"))
assert.NoError(e, err)
}
//nolint:paralleltest // uses parallel programtest
func TestStackRmConfig_LocalProject(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
testStackRmConfig(e, "organization")
}
//nolint:paralleltest // uses parallel programtest
func TestStackRmConfig_Cloud(t *testing.T) {
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
}
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
output, _ := e.RunCommand("pulumi", "whoami")
organization := strings.TrimSpace(output)
testStackRmConfig(e, organization)
}
//nolint:paralleltest // uses parallel programtest
func TestAdvisoryPolicyPack(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.ImportDirectory("single_resource")
e.ImportDirectory("policy")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
stackName, err := resource.NewUniqueHex("advisory-policy-pack", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
e.RunCommand("pulumi", "stack", "init", stackName)
_, _, err = e.GetCommandResultsIn(filepath.Join(e.CWD, "advisory_policy_pack"), "npm", "install")
assert.NoError(t, err)
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("yarn", "install")
stdout, _, err := e.GetCommandResults(
"pulumi", "up", "--skip-preview", "--yes", "--policy-pack", "advisory_policy_pack")
assert.NoError(t, err)
assert.Contains(t, stdout, "Failing advisory policy pack for testing\n foobar")
}
//nolint:paralleltest // uses parallel programtest
func TestMandatoryPolicyPack(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.ImportDirectory("single_resource")
e.ImportDirectory("policy")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
stackName, err := resource.NewUniqueHex("mandatory-policy-pack", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
e.RunCommand("pulumi", "stack", "init", stackName)
_, _, err = e.GetCommandResultsIn(filepath.Join(e.CWD, "mandatory_policy_pack"), "npm", "install")
assert.NoError(t, err)
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("yarn", "install")
stdout, _, err := e.GetCommandResults(
"pulumi", "up", "--skip-preview", "--yes", "--policy-pack", "mandatory_policy_pack")
assert.Error(t, err)
assert.Contains(t, stdout, "error: update failed")
assert.Contains(t, stdout, "❌ typescript@v0.0.1 (local: mandatory_policy_pack)")
}
//nolint:paralleltest // uses parallel programtest
func TestMultiplePolicyPacks(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.ImportDirectory("single_resource")
e.ImportDirectory("policy")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
stackName, err := resource.NewUniqueHex("multiple-policy-pack", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
e.RunCommand("pulumi", "stack", "init", stackName)
_, _, err = e.GetCommandResultsIn(filepath.Join(e.CWD, "advisory_policy_pack"), "npm", "install")
assert.NoError(t, err)
_, _, err = e.GetCommandResultsIn(filepath.Join(e.CWD, "mandatory_policy_pack"), "npm", "install")
assert.NoError(t, err)
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("yarn", "install")
stdout, _, err := e.GetCommandResults("pulumi", "up", "--skip-preview", "--yes",
"--policy-pack", "advisory_policy_pack",
"--policy-pack", "mandatory_policy_pack")
assert.Error(t, err)
assert.Contains(t, stdout, "Failing advisory policy pack for testing\n foobar")
assert.Contains(t, stdout, "error: update failed")
assert.Contains(t, stdout, "❌ typescript@v0.0.1 (local: advisory_policy_pack; mandatory_policy_pack)")
}
// regresses https://github.com/pulumi/pulumi/issues/11092
//
//nolint:paralleltest // uses parallel programtest
func TestPolicyPluginExtraArguments(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.ImportDirectory("single_resource")
e.ImportDirectory("policy")
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
stackName, err := resource.NewUniqueHex("policy-plugin-extra-args", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex should not fail with no maximum length is set")
e.RunCommand("pulumi", "stack", "init", stackName)
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("yarn", "install")
assert.NoError(t, err)
// Create a venv for the policy package and install the current python SDK into it
tc, err := toolchain.ResolveToolchain(toolchain.PythonOptions{
Toolchain: toolchain.Pip,
Root: filepath.Join(e.CWD, "python_policy_pack"),
Virtualenv: "venv",
})
require.NoError(t, err)
require.NoError(t, tc.InstallDependencies(context.Background(),
filepath.Join(e.CWD, "python_policy_pack"), false /*useLanguageVersionTools*/, false /*showOutput */, nil, nil))
sdkDir, err := filepath.Abs(filepath.Join("..", "..", "sdk", "python", "env", "src"))
require.NoError(t, err)
gotSdk, err := test.PathExists(sdkDir)
require.NoError(t, err)
if !gotSdk {
t.Log("This test requires Python SDK to be built; please `cd sdk/python && make ensure build install`")
t.FailNow()
}
cmd, err := tc.ModuleCommand(context.Background(), "pip", "install", "-e", sdkDir)
require.NoError(t, err)
require.NoError(t, cmd.Run())
// Run with extra arguments
_, _, err = e.GetCommandResults("pulumi", "--logflow", "preview", "--logtostderr",
"--policy-pack", "python_policy_pack", "--tracing", "file:/trace.log")
require.NoError(t, err)
}
//nolint:paralleltest // uses parallel programtest
func TestPolicyPackNewGenerateOnly(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
require.False(t, e.PathExists("venv"))
stdout, _ := e.RunCommand("pulumi", "policy", "new", "aws-python", "--force", "--generate-only")
require.Contains(t, stdout, "To install dependencies for the Policy Pack, run `pulumi install`")
require.False(t, e.PathExists("venv"))
}
//nolint:paralleltest // uses parallel programtest
func TestPolicyPackNew(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
require.False(t, e.PathExists("venv"))
stdout, _ := e.RunCommand("pulumi", "policy", "new", "aws-python", "--force")
require.NotContains(t, stdout, "To install dependencies for the Policy Pack, run `pulumi install`")
require.Contains(t, stdout, "Finished creating virtual environment")
require.Contains(t, stdout, "Finished installing dependencies")
require.True(t, e.PathExists("venv"))
}
//nolint:paralleltest // uses parallel programtest
func TestPolicyPackInstallDependencies(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.ImportDirectory("policy/python_policy_pack")
require.False(t, e.PathExists("venv"))
stdout, _ := e.RunCommand("pulumi", "install")
require.Contains(t, stdout, "Finished creating virtual environment")
require.Contains(t, stdout, "Finished installing dependencies")
require.True(t, e.PathExists("venv"))
}
//nolint:paralleltest // uses parallel programtest
func TestProjectInstallDependencies(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
e.ImportDirectory("single_resource")
require.False(t, e.PathExists("node_modules"))
stdout, _ := e.RunCommand("pulumi", "install")
require.Contains(t, stdout, "Finished installing dependencies")
require.True(t, e.PathExists("node_modules"))
}
//nolint:paralleltest // uses parallel programtest
func TestInstallDependenciesForPolicyPackWithParentProject(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
// create a directory structure with a pulumi project and a nested policy pack
require.NoError(t, fsutil.CopyFile(e.RootPath, "nodejs/esm-ts", nil))
policyPackPath := filepath.Join(e.RootPath, "policy")
require.NoError(t, os.Mkdir(policyPackPath, 0o700))
require.NoError(t, fsutil.CopyFile(policyPackPath, "policy/mandatory_policy_pack", nil))
// chdir to the policy pack directory and run pulumi install
e.CWD = policyPackPath
stdout, _ := e.RunCommand("pulumi", "install")
e.CWD = e.RootPath
require.Contains(t, stdout, "Finished installing dependencies")
require.False(t, e.PathExists("node_modules"), "node_modules should not exist in the root path")
require.True(t, e.PathExists(filepath.Join("policy", "node_modules")), "node_modules should exist in the policy pack")
}
//nolint:paralleltest // uses parallel programtest
func TestInstallDependenciesProjectWithParentPolicyPack(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer e.DeleteIfNotFailed()
// create a directory structure with a policy pack and a nested project
require.NoError(t, fsutil.CopyFile(e.RootPath, "policy/mandatory_policy_pack", nil))
projectPath := filepath.Join(e.RootPath, "project")
require.NoError(t, os.Mkdir(projectPath, 0o700))
require.NoError(t, fsutil.CopyFile(projectPath, "nodejs/esm-ts", nil))
// chdir to the project directory and run pulumi install
e.CWD = projectPath
stdout, _ := e.RunCommand("pulumi", "install")
e.CWD = e.RootPath
require.Contains(t, stdout, "Finished installing dependencies")
require.False(t, e.PathExists("node_modules"), "node_modules should not exist in the root path")
require.True(t, e.PathExists(filepath.Join("project", "node_modules")),
"node_modules should exist in the project path")
}