// 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 ( "context" "encoding/json" "flag" "fmt" "os" "os/exec" "path/filepath" "runtime" "syscall" pbempty "github.com/golang/protobuf/ptypes/empty" "github.com/pkg/errors" "google.golang.org/grpc" "github.com/pulumi/pulumi/pkg/util/cmdutil" "github.com/pulumi/pulumi/pkg/util/logging" "github.com/pulumi/pulumi/pkg/util/rpcutil" "github.com/pulumi/pulumi/pkg/version" "github.com/pulumi/pulumi/sdk/go/pulumi" pulumirpc "github.com/pulumi/pulumi/sdk/proto/go" ) // Launches the language host, which in turn fires up an RPC server implementing the LanguageRuntimeServer endpoint. func main() { var tracing string flag.StringVar(&tracing, "tracing", "", "Emit tracing to a Zipkin-compatible tracing endpoint") 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) 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 } func newLanguageHost(engineAddress, tracing string) pulumirpc.LanguageRuntimeServer { return &goLanguageHost{ engineAddress: engineAddress, tracing: tracing, } } // GetRequiredPlugins computes the complete set of anticipated plugins required by a program. func (host *goLanguageHost) GetRequiredPlugins(ctx context.Context, req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) { 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) { if runtime.GOOS == "windows" { program = fmt.Sprintf("%s.exe", program) } // look in the same directory cwd, err := os.Getwd() if err != nil { return "", errors.Wrap(err, "unable to get current working directory") } cwdProgram := filepath.Join(cwd, program) if fileInfo, err := os.Stat(cwdProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() { logging.V(5).Infof("program %s found in CWD", program) return cwdProgram, nil } // look in $GOPATH/bin if goPath := os.Getenv("GOPATH"); len(goPath) > 0 { goPathProgram := filepath.Join(goPath, "bin", program) if fileInfo, err := os.Stat(goPathProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() { logging.V(5).Infof("program %s found in $GOPATH/bin", program) return goPathProgram, nil } } // look in the $PATH somewhere if fullPath, err := exec.LookPath(program); err == nil { logging.V(5).Infof("program %s found in $PATH", program) return fullPath, nil } return "", errors.Errorf(unableToFindProgramTemplate, program) } // 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") } // 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 { 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 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 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 }