diff --git a/CHANGELOG.md b/CHANGELOG.md index 5238a64bc..08415767d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ CHANGELOG ## HEAD (Unreleased) +- Support for a `go run` style workflow. Building or installing a pulumi program written in go is + now optional. [3503](https://github.com/pulumi/pulumi/pull/3503) + - Re-apply "propagate resource inputs to resource state during preview, including first-class unknown values." The new set of changes have additional fixes to ensure backwards compatibility with earlier code. This allows the preview to better estimate the state of a resource after an update, including property values that were populated using defaults diff --git a/pkg/testing/integration/program.go b/pkg/testing/integration/program.go index ec09b82bd..934d01e0e 100644 --- a/pkg/testing/integration/program.go +++ b/pkg/testing/integration/program.go @@ -1608,6 +1608,12 @@ func (pt *programTester) prepareGoProject(projinfo *engine.Projinfo) error { if err != nil { return err } + + // skip building if the 'go run' invocation path is requested. + if !pt.opts.RunBuild { + return nil + } + outBin := filepath.Join(gopath, "bin", string(projinfo.Proj.Name)) return pt.runCommand("go-build", []string{goBin, "build", "-o", outBin, "."}, cwd) } diff --git a/sdk/go/pulumi-language-go/main.go b/sdk/go/pulumi-language-go/main.go index 4bb87d016..675675c14 100644 --- a/sdk/go/pulumi-language-go/main.go +++ b/sdk/go/pulumi-language-go/main.go @@ -92,6 +92,8 @@ func (host *goLanguageHost) GetRequiredPlugins(ctx context.Context, return &pulumirpc.GetRequiredPluginsResponse{}, nil } +const unableToFindProgramTemplate = "unable to find program: %s" + // findProgram attempts to find the needed program in various locations on the // filesystem, eventually resorting to searching in $PATH. func findProgram(program string) (string, error) { @@ -123,7 +125,7 @@ func findProgram(program string) (string, error) { return fullPath, nil } - return "", errors.Errorf("unable to find program: %s", program) + return "", errors.Errorf(unableToFindProgramTemplate, program) } // RPC endpoint for LanguageRuntimeServer::Run @@ -135,18 +137,51 @@ func (host *goLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest) return nil, errors.Wrap(err, "failed to prepare environment") } + // by default we try to run a named executable on the path, but we will fallback to 'go run' style execution + goRunInvoke := false + // The program to execute is simply the name of the project. This ensures good Go toolability, whereby // you can simply run `go install .` to build a Pulumi program prior to running it, among other benefits. + // For ease of use, if we don't find a pre-built program, we attempt to invoke via 'go run' on behalf of the user. program, err := findProgram(req.GetProject()) if err != nil { - return nil, errors.Wrap(err, "problem executing program (could not run language executor)") + const message = "problem executing program (could not run language executor)" + if err.Error() == fmt.Sprintf(unableToFindProgramTemplate, req.GetProject()) { + logging.V(5).Infof("Unable to find program %s in $PATH, attempting invocation via 'go run'", program) + program, err = findProgram("go") + if err != nil { + return nil, errors.Wrap(err, message) + } + goRunInvoke = true + } else { + return nil, errors.Wrap(err, message) + } } logging.V(5).Infof("language host launching process: %s", program) // Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly. var errResult string - cmd := exec.Command(program) + var cmd *exec.Cmd + + if goRunInvoke { + cwd, err := os.Getwd() + if err != nil { + return nil, errors.Wrap(err, "unable to get current working directory") + } + + goFileSearchPattern := filepath.Join(cwd, "*.go") + if matches, err := filepath.Glob(goFileSearchPattern); err != nil || len(matches) == 0 { + return nil, errors.Errorf("Failed to find go files for 'go run' matching %s", goFileSearchPattern) + } + + args := []string{"run", cwd} + // go run $cwd + cmd = exec.Command(program, args...) + } else { + cmd = exec.Command(program) + } + cmd.Env = env cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/tests/integration/empty/gorun/Pulumi.yaml b/tests/integration/empty/gorun/Pulumi.yaml new file mode 100644 index 000000000..5e815646f --- /dev/null +++ b/tests/integration/empty/gorun/Pulumi.yaml @@ -0,0 +1,3 @@ +name: emptygorun +description: An empty Go Pulumi program. +runtime: go diff --git a/tests/integration/empty/gorun/main.go b/tests/integration/empty/gorun/main.go new file mode 100644 index 000000000..2c9eb9065 --- /dev/null +++ b/tests/integration/empty/gorun/main.go @@ -0,0 +1,13 @@ +// Copyright 2016-2018, Pulumi Corporation. All rights reserved. + +package main + +import ( + "github.com/pulumi/pulumi/sdk/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + return nil + }) +} diff --git a/tests/integration/empty/gorun_main/Pulumi.yaml b/tests/integration/empty/gorun_main/Pulumi.yaml new file mode 100644 index 000000000..1979455ec --- /dev/null +++ b/tests/integration/empty/gorun_main/Pulumi.yaml @@ -0,0 +1,4 @@ +name: emptygorunmain +description: An empty Go Pulumi program. +runtime: go +main: ../gorun_main_src/ diff --git a/tests/integration/empty/gorun_main_src/main.go b/tests/integration/empty/gorun_main_src/main.go new file mode 100644 index 000000000..2c9eb9065 --- /dev/null +++ b/tests/integration/empty/gorun_main_src/main.go @@ -0,0 +1,13 @@ +// Copyright 2016-2018, Pulumi Corporation. All rights reserved. + +package main + +import ( + "github.com/pulumi/pulumi/sdk/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + return nil + }) +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 0321f1a04..b2ab15cac 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -76,10 +76,27 @@ func TestEmptyPython(t *testing.T) { }) } -// TestEmptyGo simply tests that we can run an empty Go project. +// TestEmptyGo simply tests that we can build and run an empty Go project. func TestEmptyGo(t *testing.T) { integration.ProgramTest(t, &integration.ProgramTestOptions{ - Dir: filepath.Join("empty", "go"), + Dir: filepath.Join("empty", "go"), + Quick: true, + RunBuild: true, + }) +} + +// TestEmptyGoRun exercises the 'go run' invocation path that doesn't require an explicit build step. +func TestEmptyGoRun(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("empty", "gorun"), + Quick: true, + }) +} + +// TestEmptyGoRunMain exercises the 'go run' invocation path with a 'main' entrypoint specified in Pulumi.yml +func TestEmptyGoRunMain(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("empty", "gorun_main"), Quick: true, }) } @@ -959,6 +976,7 @@ func TestConfigBasicGo(t *testing.T) { {Key: "tokens[0]", Value: "shh", Path: true, Secret: true}, {Key: "foo.bar", Value: "don't tell", Path: true, Secret: true}, }, + RunBuild: true, }) }