pulumi/sdk/go/pulumi-language-go/main_test.go

470 lines
12 KiB
Go

// 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.
package main
import (
"context"
"flag"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/testing/iotest"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseRunParams(t *testing.T) {
t.Parallel()
tests := []struct {
desc string
give []string
want runParams
wantErr string // non-empty if we expect an error
}{
{
desc: "no arguments",
wantErr: "missing required engine RPC address argument",
},
{
desc: "no options",
give: []string{"localhost:1234"},
want: runParams{
engineAddress: "localhost:1234",
},
},
{
desc: "tracing",
give: []string{"-tracing", "foo.trace", "localhost:1234"},
want: runParams{
tracing: "foo.trace",
engineAddress: "localhost:1234",
},
},
{
desc: "binary",
give: []string{"-binary", "foo", "localhost:1234"},
want: runParams{
engineAddress: "localhost:1234",
},
},
{
desc: "buildTarget",
give: []string{"-buildTarget", "foo", "localhost:1234"},
want: runParams{
engineAddress: "localhost:1234",
},
},
{
desc: "root",
give: []string{"-root", "path/to/root", "localhost:1234"},
want: runParams{
engineAddress: "localhost:1234",
},
},
{
desc: "unknown option",
give: []string{"-unknown-option", "bar", "localhost:1234"},
wantErr: "flag provided but not defined: -unknown-option",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
// Use a FlagSet with ContinueOnError for each case
// instead of using the global flag set.
//
// The global flag set uses flag.ExitOnError,
// so it cannot validate error cases during tests.
fset := flag.NewFlagSet(t.Name(), flag.ContinueOnError)
fset.SetOutput(iotest.LogWriter(t))
got, err := parseRunParams(fset, tt.give)
if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
assert.Equal(t, &tt.want, got)
}
})
}
}
func TestGetPlugin(t *testing.T) {
t.Parallel()
cases := []struct {
Name string
Mod *modInfo
Expected *pulumirpc.PluginDependency
ExpectedError string
JSON *plugin.PulumiPluginJSON
JSONPath string
}{
{
Name: "valid-pulumi-mod",
Mod: &modInfo{
Path: "github.com/pulumi/pulumi-aws/sdk",
Version: "v1.29.0",
},
Expected: &pulumirpc.PluginDependency{
Name: "aws",
Version: "v1.29.0",
},
},
{
Name: "pulumi-pseduo-version-plugin",
Mod: &modInfo{
Path: "github.com/pulumi/pulumi-aws/sdk",
Version: "v1.29.1-0.20200403140640-efb5e2a48a86",
},
Expected: &pulumirpc.PluginDependency{
Name: "aws",
Version: "v1.29.0",
},
},
{
Name: "non-pulumi-mod",
Mod: &modInfo{
Path: "github.com/moolumi/pulumi-aws/sdk",
Version: "v1.29.0",
},
ExpectedError: "module is not a pulumi provider",
},
{
Name: "invalid-version-module",
Mod: &modInfo{
Path: "github.com/pulumi/pulumi-aws/sdk",
Version: "42-42-42",
},
ExpectedError: "module does not have semver compatible version",
},
{
Name: "pulumi-pulumi-mod",
Mod: &modInfo{
Path: "github.com/pulumi/pulumi/sdk",
Version: "v1.14.0",
},
ExpectedError: "module is not a pulumi provider",
},
{
Name: "beta-pulumi-module",
Mod: &modInfo{
Path: "github.com/pulumi/pulumi-aws/sdk",
Version: "v2.0.0-beta.1",
},
Expected: &pulumirpc.PluginDependency{
Name: "aws",
Version: "v2.0.0-beta.1",
},
},
{
Name: "non-zero-patch-module", Mod: &modInfo{
Path: "github.com/pulumi/pulumi-kubernetes/sdk",
Version: "v1.5.8",
},
Expected: &pulumirpc.PluginDependency{
Name: "kubernetes",
Version: "v1.5.8",
},
},
{
Name: "pulumiplugin",
Mod: &modInfo{
Path: "github.com/me/myself/i",
Version: "invalid-Version",
},
Expected: &pulumirpc.PluginDependency{
Name: "thing1",
Version: "v1.2.3",
Server: "myserver.com",
},
JSON: &plugin.PulumiPluginJSON{
Resource: true,
Name: "thing1",
Version: "v1.2.3",
Server: "myserver.com",
},
},
{
Name: "non-resource",
Mod: &modInfo{},
ExpectedError: "module is not a pulumi provider",
JSON: &plugin.PulumiPluginJSON{
Resource: false,
},
},
{
Name: "missing-pulumiplugin",
Mod: &modInfo{
Dir: "/not/real",
},
ExpectedError: "module is not a pulumi provider",
JSON: &plugin.PulumiPluginJSON{
Name: "thing2",
Version: "v1.2.3",
},
},
{
Name: "pulumiplugin-go-lookup",
Mod: &modInfo{
Path: "github.com/me/myself",
Version: "v1.2.3",
},
JSON: &plugin.PulumiPluginJSON{
Name: "name",
Resource: true,
},
JSONPath: "go",
Expected: &pulumirpc.PluginDependency{
Name: "name",
Version: "v1.2.3",
},
},
{
Name: "pulumiplugin-go-name-lookup",
Mod: &modInfo{
Path: "github.com/me/myself",
Version: "v1.2.3",
},
JSON: &plugin.PulumiPluginJSON{
Name: "name",
Resource: true,
},
JSONPath: filepath.Join("go", "name"),
Expected: &pulumirpc.PluginDependency{
Name: "name",
Version: "v1.2.3",
},
},
{
Name: "pulumiplugin-nested-too-deep",
Mod: &modInfo{
Path: "path.com/here",
Version: "v0.0",
},
JSONPath: filepath.Join("go", "valid", "invalid"),
JSON: &plugin.PulumiPluginJSON{
Name: "name",
Resource: true,
},
ExpectedError: "module is not a pulumi provider",
},
{
Name: "nested-wrong-folder",
Mod: &modInfo{
Path: "path.com/here",
Version: "v0.0",
},
JSONPath: filepath.Join("invalid", "valid"),
JSON: &plugin.PulumiPluginJSON{
Name: "name",
Resource: true,
},
ExpectedError: "module is not a pulumi provider",
},
}
for _, c := range cases {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
cwd := t.TempDir()
if c.Mod.Dir == "" {
c.Mod.Dir = cwd
}
if c.JSON != nil {
path := filepath.Join(cwd, c.JSONPath)
err := os.MkdirAll(path, 0o700)
assert.NoErrorf(t, err, "Failed to setup test folder %s", path)
bytes, err := c.JSON.JSON()
assert.NoError(t, err, "Failed to setup test pulumi-plugin.json")
err = os.WriteFile(filepath.Join(path, "pulumi-plugin.json"), bytes, 0o600)
assert.NoError(t, err, "Failed to write pulumi-plugin.json")
}
actual, err := c.Mod.getPlugin(t.TempDir())
if c.ExpectedError != "" {
assert.EqualError(t, err, c.ExpectedError)
} else {
// Kind must be resource. We can thus exclude it from the test.
if c.Expected.Kind == "" {
c.Expected.Kind = "resource"
}
assert.NoError(t, err)
assert.Equal(t, c.Expected, actual)
}
})
}
}
func TestPluginsAndDependencies_moduleMode(t *testing.T) {
t.Parallel()
root := t.TempDir()
require.NoError(t,
fsutil.CopyFile(root, filepath.Join("testdata", "sample"), nil),
"copy test data")
testPluginsAndDependencies(t, filepath.Join(root, "prog"))
}
// Test for https://github.com/pulumi/pulumi/issues/12526.
// Validates that if a Pulumi program has vendored its dependencies,
// the language host can still find the plugin and run the program.
func TestPluginsAndDependencies_vendored(t *testing.T) {
t.Parallel()
root := t.TempDir()
require.NoError(t,
fsutil.CopyFile(root, filepath.Join("testdata", "sample"), nil),
"copy test data")
progDir := filepath.Join(root, "prog")
// Vendor the dependencies and nuke the sources
// to ensure that the language host can only use the vendored version.
cmd := exec.Command("go", "mod", "vendor")
cmd.Dir = progDir
cmd.Stdout = iotest.LogWriter(t)
cmd.Stderr = iotest.LogWriter(t)
require.NoError(t, cmd.Run(), "vendor dependencies")
require.NoError(t, os.RemoveAll(filepath.Join(root, "plugin")))
require.NoError(t, os.RemoveAll(filepath.Join(root, "dep")))
require.NoError(t, os.RemoveAll(filepath.Join(root, "indirect-dep")))
testPluginsAndDependencies(t, progDir)
}
// Regression test for https://github.com/pulumi/pulumi/issues/12963.
// Verifies that the language host can find plugins and dependencies
// when the Pulumi program is in a subdirectory of the project root.
func TestPluginsAndDependencies_subdir(t *testing.T) {
t.Parallel()
t.Run("moduleMode", func(t *testing.T) {
t.Parallel()
root := t.TempDir()
require.NoError(t,
fsutil.CopyFile(root, filepath.Join("testdata", "sample"), nil),
"copy test data")
testPluginsAndDependencies(t, filepath.Join(root, "prog-subdir", "infra"))
})
t.Run("vendored", func(t *testing.T) {
t.Parallel()
root := t.TempDir()
require.NoError(t,
fsutil.CopyFile(root, filepath.Join("testdata", "sample"), nil),
"copy test data")
progDir := filepath.Join(root, "prog-subdir", "infra")
// Vendor the dependencies and nuke the sources
// to ensure that the language host can only use the vendored version.
cmd := exec.Command("go", "mod", "vendor")
cmd.Dir = progDir
cmd.Stdout = iotest.LogWriter(t)
cmd.Stderr = iotest.LogWriter(t)
require.NoError(t, cmd.Run(), "vendor dependencies")
require.NoError(t, os.RemoveAll(filepath.Join(root, "plugin")))
require.NoError(t, os.RemoveAll(filepath.Join(root, "dep")))
require.NoError(t, os.RemoveAll(filepath.Join(root, "indirect-dep")))
testPluginsAndDependencies(t, progDir)
})
t.Run("gowork", func(t *testing.T) {
t.Parallel()
root := t.TempDir()
require.NoError(t,
fsutil.CopyFile(root, filepath.Join("testdata", "sample"), nil),
"copy test data")
testPluginsAndDependencies(t, filepath.Join(root, "prog-gowork", "prog"))
})
}
func testPluginsAndDependencies(t *testing.T, progDir string) {
host := newLanguageHost("0.0.0.0:0", progDir, "")
ctx := context.Background()
t.Run("GetRequiredPlugins", func(t *testing.T) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
res, err := host.GetRequiredPlugins(ctx, &pulumirpc.GetRequiredPluginsRequest{
Project: "deprecated",
Pwd: progDir,
Info: &pulumirpc.ProgramInfo{
RootDirectory: progDir,
ProgramDirectory: progDir,
EntryPoint: ".",
},
})
require.NoError(t, err)
require.Len(t, res.Plugins, 1)
plug := res.Plugins[0]
assert.Equal(t, "example", plug.Name, "plugin name")
assert.Equal(t, "v1.2.3", plug.Version, "plugin version")
assert.Equal(t, "resource", plug.Kind, "plugin kind")
assert.Equal(t, "example.com/download", plug.Server, "plugin server")
})
t.Run("GetProgramDependencies", func(t *testing.T) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
res, err := host.GetProgramDependencies(ctx, &pulumirpc.GetProgramDependenciesRequest{
Project: "deprecated",
Pwd: progDir,
TransitiveDependencies: true,
Info: &pulumirpc.ProgramInfo{
RootDirectory: progDir,
ProgramDirectory: progDir,
EntryPoint: ".",
},
})
require.NoError(t, err)
gotDeps := make(map[string]string) // name => version
for _, dep := range res.Dependencies {
gotDeps[dep.Name] = dep.Version
}
assert.Equal(t, map[string]string{
"github.com/pulumi/go-dependency-testdata/plugin": "v1.2.3",
"github.com/pulumi/go-dependency-testdata/dep": "v1.6.0",
"github.com/pulumi/go-dependency-testdata/indirect-dep/v2": "v2.1.0",
}, gotDeps)
})
}