* Propagate workspace.Project metadata to plugin init * Get to a working fix * Propagate Root via plugin context * Propagate root instead of yaml path * Revert out unnecessary parameter propagation * Root is now always absolute at this point; simplify code and docs * Drop python conditional and propagate unused -root to all lang hosts * Add tests that fail before and pass after * Lint * Add changelog entry
325 lines
11 KiB
Go
325 lines
11 KiB
Go
// Copyright 2016-2018, 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 (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/blang/semver"
|
|
pbempty "github.com/golang/protobuf/ptypes/empty"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/buildutil"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/executable"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/goversion"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/version"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
|
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
|
|
)
|
|
|
|
func findProgram(binary string) (*exec.Cmd, error) {
|
|
// we default to execution via `go run`
|
|
// the user can explicitly opt in to using a binary executable by specifying
|
|
// runtime.options.binary in the Pulumi.yaml
|
|
if binary != "" {
|
|
program, err := executable.FindExecutable(binary)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "expected to find prebuilt executable")
|
|
}
|
|
return exec.Command(program), nil
|
|
}
|
|
|
|
// Fall back to 'go run' style executions
|
|
logging.V(5).Infof("No prebuilt executable specified, attempting invocation via 'go run'")
|
|
program, err := executable.FindExecutable("go")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "problem executing program (could not run language executor)")
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return exec.Command(program, "run", cwd), nil
|
|
}
|
|
|
|
// Launches the language host, which in turn fires up an RPC server implementing the LanguageRuntimeServer endpoint.
|
|
func main() {
|
|
var tracing string
|
|
var binary string
|
|
var root string
|
|
flag.StringVar(&tracing, "tracing", "", "Emit tracing to a Zipkin-compatible tracing endpoint")
|
|
flag.StringVar(&binary, "binary", "", "Look on path for a binary executable with this name")
|
|
flag.StringVar(&root, "root", "", "Project root path to use")
|
|
|
|
flag.Parse()
|
|
args := flag.Args()
|
|
logging.InitLogging(false, 0, false)
|
|
cmdutil.InitTracing("pulumi-language-go", "pulumi-language-go", tracing)
|
|
|
|
// Pluck out the engine so we can do logging, etc.
|
|
if len(args) == 0 {
|
|
cmdutil.Exit(errors.New("missing required engine RPC address argument"))
|
|
}
|
|
engineAddress := args[0]
|
|
|
|
// Fire up a gRPC server, letting the kernel choose a free port.
|
|
port, done, err := rpcutil.Serve(0, nil, []func(*grpc.Server) error{
|
|
func(srv *grpc.Server) error {
|
|
host := newLanguageHost(engineAddress, tracing, binary)
|
|
pulumirpc.RegisterLanguageRuntimeServer(srv, host)
|
|
return nil
|
|
},
|
|
}, nil)
|
|
if err != nil {
|
|
cmdutil.Exit(errors.Wrapf(err, "could not start language host RPC server"))
|
|
}
|
|
|
|
// Otherwise, print out the port so that the spawner knows how to reach us.
|
|
fmt.Printf("%d\n", port)
|
|
|
|
// And finally wait for the server to stop serving.
|
|
if err := <-done; err != nil {
|
|
cmdutil.Exit(errors.Wrapf(err, "language host RPC stopped serving"))
|
|
}
|
|
}
|
|
|
|
// goLanguageHost implements the LanguageRuntimeServer interface for use as an API endpoint.
|
|
type goLanguageHost struct {
|
|
engineAddress string
|
|
tracing string
|
|
binary string
|
|
}
|
|
|
|
func newLanguageHost(engineAddress, tracing, binary string) pulumirpc.LanguageRuntimeServer {
|
|
return &goLanguageHost{
|
|
engineAddress: engineAddress,
|
|
tracing: tracing,
|
|
binary: binary,
|
|
}
|
|
}
|
|
|
|
// modInfo is the useful portion of the output from `go list -m -json all`
|
|
// with respect to plugin acquisition
|
|
type modInfo struct {
|
|
Path string
|
|
Version string
|
|
}
|
|
|
|
func (m *modInfo) getPlugin() (*pulumirpc.PluginDependency, error) {
|
|
if !strings.HasPrefix(m.Path, "github.com/pulumi/pulumi-") {
|
|
return nil, errors.New("module is not a pulumi provider")
|
|
}
|
|
|
|
// github.com/pulumi/pulumi-aws/sdk/... => aws
|
|
pluginPart := strings.Split(m.Path, "/")[2]
|
|
name := strings.SplitN(pluginPart, "-", 2)[1]
|
|
|
|
v, err := semver.ParseTolerant(m.Version)
|
|
if err != nil {
|
|
return nil, errors.New("module does not have semver compatible version")
|
|
}
|
|
version := m.Version
|
|
|
|
// psuedoversions are commits that don't have a corresponding tag at the specified git hash
|
|
// https://golang.org/cmd/go/#hdr-Pseudo_versions
|
|
// pulumi-aws v1.29.1-0.20200403140640-efb5e2a48a86 (first commit after 1.29.0 release)
|
|
if buildutil.IsPseudoVersion(version) {
|
|
// no prior tag means there was never a release build
|
|
if v.Major == 0 && v.Minor == 0 && v.Patch == 0 {
|
|
return nil, errors.New("invalid pseduoversion with no prior tag")
|
|
}
|
|
// patch is typically bumped from the previous tag when using pseudo version
|
|
// downgrade the patch by 1 to make sure we match a release that exists
|
|
patch := v.Patch
|
|
if patch > 0 {
|
|
patch--
|
|
}
|
|
version = fmt.Sprintf("v%v.%v.%v", v.Major, v.Minor, patch)
|
|
}
|
|
|
|
plugin := &pulumirpc.PluginDependency{
|
|
Name: name,
|
|
Version: version,
|
|
Kind: "resource",
|
|
}
|
|
|
|
return plugin, nil
|
|
}
|
|
|
|
// GetRequiredPlugins computes the complete set of anticipated plugins required by a program.
|
|
// We're lenient here as this relies on the `go list` command and the use of modules.
|
|
// If the consumer insists on using some other form of dependency management tool like
|
|
// dep or glide, the list command fails with "go list -m: not using modules".
|
|
// However, we do enforce that go 1.14.0 or higher is installed.
|
|
func (host *goLanguageHost) GetRequiredPlugins(ctx context.Context,
|
|
req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) {
|
|
|
|
logging.V(5).Infof("GetRequiredPlugins: Determining pulumi packages")
|
|
|
|
gobin, err := executable.FindExecutable("go")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "couldn't find go binary")
|
|
}
|
|
|
|
if err = goversion.CheckMinimumGoVersion(gobin); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// don't wire up stderr so non-module users don't see error output from list
|
|
cmd := exec.Command(gobin, "list", "-m", "-json", "-mod=mod", "all")
|
|
cmd.Env = os.Environ()
|
|
stdout, err := cmd.Output()
|
|
if err != nil {
|
|
logging.V(5).Infof("GetRequiredPlugins: Error discovering plugin requirements using go modules: %s", err.Error())
|
|
return &pulumirpc.GetRequiredPluginsResponse{}, nil
|
|
}
|
|
|
|
plugins := []*pulumirpc.PluginDependency{}
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(stdout))
|
|
for {
|
|
var m modInfo
|
|
if err := dec.Decode(&m); err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
logging.V(5).Infof("GetRequiredPlugins: Error parsing list output: %s", err.Error())
|
|
return &pulumirpc.GetRequiredPluginsResponse{}, nil
|
|
}
|
|
|
|
plugin, err := m.getPlugin()
|
|
if err == nil {
|
|
logging.V(5).Infof("GetRequiredPlugins: Found plugin name: %s, version: %s", plugin.Name, plugin.Version)
|
|
plugins = append(plugins, plugin)
|
|
} else {
|
|
logging.V(5).Infof(
|
|
"GetRequiredPlugins: Ignoring dependency: %s, version: %s, error: %s",
|
|
m.Path,
|
|
m.Version,
|
|
err.Error(),
|
|
)
|
|
}
|
|
}
|
|
|
|
return &pulumirpc.GetRequiredPluginsResponse{
|
|
Plugins: plugins,
|
|
}, nil
|
|
}
|
|
|
|
// RPC endpoint for LanguageRuntimeServer::Run
|
|
func (host *goLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) {
|
|
// Create the environment we'll use to run the process. This is how we pass the RunInfo to the actual
|
|
// Go program runtime, to avoid needing any sort of program interface other than just a main entrypoint.
|
|
env, err := host.constructEnv(req)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to prepare environment")
|
|
}
|
|
|
|
cmd, err := findProgram(host.binary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.Env = env
|
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
|
|
var errResult string
|
|
if err := cmd.Run(); err != nil {
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
// If the program ran, but exited with a non-zero error code. This will happen often, since user
|
|
// errors will trigger this. So, the error message should look as nice as possible.
|
|
if status, stok := exiterr.Sys().(syscall.WaitStatus); stok {
|
|
err = errors.Errorf("program exited with non-zero exit code: %d", status.ExitStatus())
|
|
} else {
|
|
err = errors.Wrapf(exiterr, "program exited unexpectedly")
|
|
}
|
|
} else {
|
|
// Otherwise, we didn't even get to run the program. This ought to never happen unless there's
|
|
// a bug or system condition that prevented us from running the language exec. Issue a scarier error.
|
|
err = errors.Wrapf(err, "problem executing program (could not run language executor)")
|
|
}
|
|
|
|
errResult = err.Error()
|
|
}
|
|
|
|
return &pulumirpc.RunResponse{Error: errResult}, nil
|
|
}
|
|
|
|
// constructEnv constructs an environment for a Go progam by enumerating all of the optional and non-optional
|
|
// arguments present in a RunRequest.
|
|
func (host *goLanguageHost) constructEnv(req *pulumirpc.RunRequest) ([]string, error) {
|
|
config, err := host.constructConfig(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
env := os.Environ()
|
|
maybeAppendEnv := func(k, v string) {
|
|
if v != "" {
|
|
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
}
|
|
|
|
maybeAppendEnv(pulumi.EnvProject, req.GetProject())
|
|
maybeAppendEnv(pulumi.EnvStack, req.GetStack())
|
|
maybeAppendEnv(pulumi.EnvConfig, config)
|
|
maybeAppendEnv(pulumi.EnvDryRun, fmt.Sprintf("%v", req.GetDryRun()))
|
|
maybeAppendEnv(pulumi.EnvParallel, fmt.Sprint(req.GetParallel()))
|
|
maybeAppendEnv(pulumi.EnvMonitor, req.GetMonitorAddress())
|
|
maybeAppendEnv(pulumi.EnvEngine, host.engineAddress)
|
|
|
|
return env, nil
|
|
}
|
|
|
|
// constructConfig JSON-serializes the configuration data given as part of a RunRequest.
|
|
func (host *goLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) {
|
|
configMap := req.GetConfig()
|
|
if configMap == nil {
|
|
return "", nil
|
|
}
|
|
|
|
configJSON, err := json.Marshal(configMap)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(configJSON), nil
|
|
}
|
|
|
|
func (host *goLanguageHost) GetPluginInfo(ctx context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
|
|
return &pulumirpc.PluginInfo{
|
|
Version: version.Version,
|
|
}, nil
|
|
}
|