pulumi/sdk/go/pulumi-language-go/main.go
joeduffy 2e8bbcc9dd Add output marshaling and improve input marsalling
This change primarily does two things:

* Adds output marshaling.

* Adds tests for roundtripping inputs to outputs.

It also

* Fixes a bug in the verification of asset archives.

* Change input types to simply `interface{}` and `map[string]interface{}`.
  There is no need for wrapper types.  This is more idiomatic.

* Reject output properties upon marshaling failure.

* Don't support time.Time as a marshaling concept.  This was getting too
  cute.  It's not clear what its marshaling format ought to be.
2018-06-09 09:11:35 -07:00

180 lines
6.2 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 (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"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
},
})
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
}
// 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")
}
// 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.
program := req.GetProject()
logging.V(5).Infoln("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) // nolint: gas, intentionally running dynamic program name.
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
}
var env []string
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
}