From 7d8995991b7a4f8ca2e10ba0e2301107b6417868 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 3 Jun 2018 10:37:26 -0700 Subject: [PATCH 01/19] Support Pulumi programs written in Go This adds rudimentary support for Pulumi programs written in Go. It is not complete yet but the basic resource registration works. Note that, stylistically speaking, Go is a bit different from our other languages. This made it a bit easier to build this initial prototype, since what we want is actually a rather thin veneer atop our existing RPC interfaces. The lack of generics, however, adds some friction and is something I'm continuing to hammer on; this will most likely lead to little specialized types (e.g. StringOutput) once the dust settles. There are two primary components: 1) A new language host, `pulumi-language-go`, which is responsible for communicating with the engine through the usual gRPC interfaces. Because Go programs are pre-compiled, it very simply loads a binary with the same name as the project. 2) A client SDK library that Pulumi programs bind against. This exports the core resource types -- including assets -- properties -- including output properties -- and configuration. Most remaining TODOs are marked as such in the code, and this will not be merged until they have been addressed, and some better tests written. --- pkg/resource/properties.go | 2 + sdk/go/README.md | 16 ++ sdk/go/pulumi-language-go/main.go | 181 ++++++++++++++++ sdk/go/pulumi/asset/asset.go | 95 +++++++++ sdk/go/pulumi/config/config.go | 15 ++ sdk/go/pulumi/context.go | 339 ++++++++++++++++++++++++++++++ sdk/go/pulumi/properties.go | 140 ++++++++++++ sdk/go/pulumi/resource.go | 54 +++++ sdk/go/pulumi/rpc.go | 175 +++++++++++++++ sdk/go/pulumi/run.go | 133 ++++++++++++ 10 files changed, 1150 insertions(+) create mode 100644 sdk/go/README.md create mode 100644 sdk/go/pulumi-language-go/main.go create mode 100644 sdk/go/pulumi/asset/asset.go create mode 100644 sdk/go/pulumi/config/config.go create mode 100644 sdk/go/pulumi/context.go create mode 100644 sdk/go/pulumi/properties.go create mode 100644 sdk/go/pulumi/resource.go create mode 100644 sdk/go/pulumi/rpc.go create mode 100644 sdk/go/pulumi/run.go diff --git a/pkg/resource/properties.go b/pkg/resource/properties.go index 0a80e1f29..a50c6d7f9 100644 --- a/pkg/resource/properties.go +++ b/pkg/resource/properties.go @@ -284,6 +284,8 @@ func NewPropertyValueRepl(v interface{}, obj[pk] = pv } return NewObjectProperty(obj) + case reflect.String: + return NewStringProperty(rv.String()) case reflect.Struct: obj := NewPropertyMapRepl(v, replk, replv) return NewObjectProperty(obj) diff --git a/sdk/go/README.md b/sdk/go/README.md new file mode 100644 index 000000000..438ffc2f7 --- /dev/null +++ b/sdk/go/README.md @@ -0,0 +1,16 @@ +# Pulumi Golang SDK + +This directory contains support for writing Pulumi programs in the Go language. There are two aspects to this: + +* `pulumi/` contains the client language bindings Pulumi program's code directly against; +* `pulumi-language-go/` contains the language host plugin that the Pulumi engine uses to orchestrate updates. + +To author a Pulumi program in Go, simply say so in your `Pulumi.yaml` + + name: + runtime: go + +and ensure you have `pulumi-language-go` on your path (it is distributed in the Pulumi download automatically). + +By default, the language plugin will use your project's name, ``, as the executable that it loads. This too +must be on your path for the language provider to load it when you run `pulumi preview` or `pulumi update`. diff --git a/sdk/go/pulumi-language-go/main.go b/sdk/go/pulumi-language-go/main.go new file mode 100644 index 000000000..325ddb7e0 --- /dev/null +++ b/sdk/go/pulumi-language-go/main.go @@ -0,0 +1,181 @@ +// 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 +} diff --git a/sdk/go/pulumi/asset/asset.go b/sdk/go/pulumi/asset/asset.go new file mode 100644 index 000000000..02da0b1a4 --- /dev/null +++ b/sdk/go/pulumi/asset/asset.go @@ -0,0 +1,95 @@ +// 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 asset + +import ( + "reflect" + + "github.com/pulumi/pulumi/pkg/util/contract" +) + +// Asset represents a file that is managed in conjunction with Pulumi resources. An Asset may be backed by a number +// of sources, including local filesystem paths, in-memory blobs of text, or remote files referenced by a URL. +type Asset interface { + // Path returns the filesystem path, for file-based assets. + Path() string + // Text returns an in-memory blob of text, for string-based assets. + Text() string + // URI returns a URI, for remote network-based assets. + URI() string +} + +type asset struct { + path string + text string + uri string +} + +func NewFileAsset(path string) Asset { + return &asset{path: path} +} + +func NewStringAsset(text string) Asset { + return &asset{text: text} +} + +func NewRemoteAsset(uri string) Asset { + return &asset{uri: uri} +} + +func (a *asset) Path() string { return a.path } +func (a *asset) Text() string { return a.text } +func (a *asset) URI() string { return a.uri } + +// Archive represents a collection of Assets. +type Archive interface { + // Assets returns a map of named assets or archives, for collections. + Assets() map[string]interface{} + // Path returns the filesystem path, for file-based archives. + Path() string + // URI returns a URI, for remote network-based archives. + URI() string +} + +type archive struct { + assets map[string]interface{} + path string + uri string +} + +// NewAssetArchive creates a new archive from an in-memory collection of named assets or other archives. +func NewAssetArchive(assets map[string]interface{}) Archive { + for k, a := range assets { + if _, ok := a.(*Asset); !ok { + if _, ok2 := a.(*Archive); !ok2 { + contract.Failf( + "expected asset map to contain *Assets and/or *Archives; %s is %v", k, reflect.TypeOf(a)) + } + } + } + return &archive{assets: assets} +} + +func NewPathArchive(path string) Archive { + return &archive{path: path} +} + +func NewURIArchive(uri string) Archive { + return &archive{uri: uri} +} + +func (a *archive) Assets() map[string]interface{} { return a.assets } +func (a *archive) Path() string { return a.path } +func (a *archive) URI() string { return a.uri } diff --git a/sdk/go/pulumi/config/config.go b/sdk/go/pulumi/config/config.go new file mode 100644 index 000000000..28d179d46 --- /dev/null +++ b/sdk/go/pulumi/config/config.go @@ -0,0 +1,15 @@ +// 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 pulumi diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go new file mode 100644 index 000000000..16f32cdac --- /dev/null +++ b/sdk/go/pulumi/context.go @@ -0,0 +1,339 @@ +// 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 pulumi + +import ( + "sort" + "sync" + + "github.com/golang/glog" + "github.com/pkg/errors" + "golang.org/x/net/context" + "google.golang.org/grpc" + + "github.com/pulumi/pulumi/pkg/resource" + "github.com/pulumi/pulumi/pkg/resource/plugin" + pulumirpc "github.com/pulumi/pulumi/sdk/proto/go" +) + +// Context handles registration of resources and exposes metadata about the current deployment context. +type Context struct { + ctx context.Context + info RunInfo + stackR URN + exports Inputs + monitor pulumirpc.ResourceMonitorClient + monitorConn *grpc.ClientConn + engine pulumirpc.EngineClient + engineConn *grpc.ClientConn + rpcs int // the number of outstanding RPC requests. + rpcsDone *sync.Cond // an event signaling completion of RPCs. + rpcsLock *sync.Mutex // a lock protecting the RPC count and event. +} + +// NewContext creates a fresh run context out of the given metadata. +func NewContext(ctx context.Context, info RunInfo) (*Context, error) { + // Validate some properties. + if info.Project == "" { + return nil, errors.New("missing project name") + } + if info.Stack == "" { + return nil, errors.New("missing stack name") + } + if info.MonitorAddr == "" { + return nil, errors.New("missing resource monitor RPC address") + } + if info.EngineAddr == "" { + return nil, errors.New("missing engine RPC address") + } + + monitorConn, err := grpc.Dial(info.MonitorAddr, grpc.WithInsecure()) + if err != nil { + return nil, errors.Wrap(err, "connecting to resource monitor over RPC") + } + + engineConn, err := grpc.Dial(info.EngineAddr, grpc.WithInsecure()) + if err != nil { + return nil, errors.Wrap(err, "connecting to engine over RPC") + } + + mutex := &sync.Mutex{} + return &Context{ + ctx: ctx, + info: info, + exports: make(Inputs), + monitorConn: monitorConn, + monitor: pulumirpc.NewResourceMonitorClient(monitorConn), + engineConn: engineConn, + engine: pulumirpc.NewEngineClient(engineConn), + rpcs: 0, + rpcsLock: mutex, + rpcsDone: sync.NewCond(mutex), + }, nil +} + +// Close implements io.Closer and relinquishes any outstanding resources held by the context. +func (ctx *Context) Close() error { + if err := ctx.engineConn.Close(); err != nil { + return err + } + if err := ctx.monitorConn.Close(); err != nil { + return err + } + return nil +} + +// Project returns the current project name. +func (ctx *Context) Project() string { return ctx.info.Project } + +// Stack returns the current stack name being deployed into. +func (ctx *Context) Stack() string { return ctx.info.Stack } + +// Parallel returns the degree of parallelism currently being used by the engine (1 being entirely serial). +func (ctx *Context) Parallel() int { return ctx.info.Parallel } + +// DryRun is true when evaluating a program for purposes of planning, instead of performing a true deployment. +func (ctx *Context) DryRun() bool { return ctx.info.DryRun } + +// Invoke will invoke a provider's function, identified by its token tok. +func (ctx *Context) Invoke(tok string, args Inputs) (Outputs, error) { + // TODO(joe): implement this. + return nil, errors.New("Invoke not yet implemented") +} + +// ReadResource reads an existing custom resource's state from the resource monitor. Note that resources read in this +// way will not be part of the resulting stack's state, as they are presumed to belong to another. +func (ctx *Context) ReadResource( + t, name string, id ID, state Inputs, opts ...ResourceOpt) (*ResourceState, error) { + if t == "" { + return nil, errors.New("resource type argument cannot be empty") + } else if name == "" { + return nil, errors.New("resource name argument (for URN creation) cannot be empty") + } else if id == "" { + return nil, errors.New("resource ID is required for lookup and cannot be empty") + } + + return nil, errors.New("ReadResource not yet implemented") +} + +// RegisterResource creates and registers a new resource object. t is the fully qualified type token and name is +// the "name" part to use in creating a stable and globally unique URN for the object. state contains the goal state +// for the resource object and opts contains optional settings that govern the way the resource is created. +func (ctx *Context) RegisterResource( + t, name string, custom bool, props Inputs, opts ...ResourceOpt) (*ResourceState, error) { + if t == "" { + return nil, errors.New("resource type argument cannot be empty") + } else if name == "" { + return nil, errors.New("resource name argument (for URN creation) cannot be empty") + } + + // Get the parent and dependency URNs from the options, in addition to the protection bit. If there wasn't an + // explicit parent, and a root stack resource exists, we will automatically parent to that. + parentURN, optDepURNs, protect := ctx.getOpts(opts...) + + // Serialize all properties, first by awaiting them, and then marshaling them to the requisite gRPC values. + keys, rpcProps, rpcDepURNs, err := marshalInputs(props) + if err != nil { + return nil, errors.Wrap(err, "marshaling properties") + } + + // Merge all dependencies with what we got earlier from property marshaling, and remove duplicates. + var depURNs []string + depMap := make(map[URN]bool) + for _, dep := range append(optDepURNs, rpcDepURNs...) { + if _, has := depMap[dep]; !has { + depURNs = append(depURNs, string(dep)) + depMap[dep] = true + } + } + sort.Strings(depURNs) + + // Create a set of resolvers that we'll use to finalize state, for URNs, IDs, and output properties. + urn, resolveURN, rejectURN := NewOutput(nil) + + var id *Output + var resolveID func(interface{}) + var rejectID func(error) + if custom { + id, resolveID, rejectID = NewOutput(nil) + } + + state := make(map[string]*Output) + resolveState := make(map[string]func(interface{})) + rejectState := make(map[string]func(error)) + for _, key := range keys { + state[key], resolveState[key], rejectState[key] = NewOutput(nil) + } + + // Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown. + if err = ctx.beginRPC(); err != nil { + return nil, err + } + + // Kick off the resource registration. If we are actually performing a deployment, the resulting properties + // will be resolved asynchronously as the RPC operation completes. If we're just planning, values won't resolve. + go func() { + glog.V(9).Infof("RegisterResource(%s, %s): Goroutine spawned, RPC call being made", t, name) + resp, err := ctx.monitor.RegisterResource(ctx.ctx, &pulumirpc.RegisterResourceRequest{ + Type: t, + Name: name, + Parent: string(parentURN), + Object: rpcProps, + Custom: custom, + Protect: protect, + Dependencies: depURNs, + }) + var outprops map[string]interface{} + if err == nil { + var uprops resource.PropertyMap + if uprops, err = plugin.UnmarshalProperties(resp.Object, plugin.MarshalOptions{}); err == nil { + outprops = uprops.Mappable() + } + } + if err != nil { + glog.V(9).Infof("RegisterResource(%s, %s): error: %v", t, name, err) + rejectURN(err) + if rejectID != nil { + rejectID(err) + } + for _, reject := range rejectState { + reject(err) + } + } else { + glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s %d", resp.Urn, resp.Id, len(outprops)) + resolveURN(URN(resp.Urn)) + if resolveID != nil { + resolveID(ID(resp.Id)) + } + for _, key := range keys { + // TODO(joe): check for missing keys, etc. + resolveState[key](outprops[key]) + } + } + + // Signal the completion of this RPC and notify any potential awaiters. + ctx.endRPC() + }() + + return &ResourceState{ + URN: urn, + ID: id, + State: state, + }, nil +} + +// getOpts returns a set of resource options from an array of them. This includes the parent URN, any +// dependency URNs, and a boolean indicating whether the resource is to be protected. +func (ctx *Context) getOpts(opts ...ResourceOpt) (URN, []URN, bool) { + return ctx.getOptsParentURN(opts...), + ctx.getOptsDepURNs(opts...), + ctx.getOptsProtect(opts...) +} + +// getOptsParentURN returns a URN to use for a resource, given its options, defaulting to the current stack resource. +func (ctx *Context) getOptsParentURN(opts ...ResourceOpt) URN { + for _, opt := range opts { + if opt.Parent != nil { + return opt.Parent.URN() + } + } + return ctx.stackR +} + +// getOptsDepURNs returns the set of dependency URNs in a resource's options. +func (ctx *Context) getOptsDepURNs(opts ...ResourceOpt) []URN { + var urns []URN + for _, opt := range opts { + for _, dep := range opt.DependsOn { + urns = append(urns, dep.URN()) + } + } + return urns +} + +// getOptsProtect returns true if a resource's options indicate that it is to be protected. +func (ctx *Context) getOptsProtect(opts ...ResourceOpt) bool { + for _, opt := range opts { + if opt.Protect { + return true + } + } + return false +} + +// noMoreRPCs is a sentinel value used to stop subsequent RPCs from occurring. +const noMoreRPCs = -1 + +// beginRPC attempts to start a new RPC request, returning a non-nil error if no more RPCs are permitted +// (usually because the program is shutting down). +func (ctx *Context) beginRPC() error { + ctx.rpcsLock.Lock() + defer ctx.rpcsLock.Unlock() + + // If we're done with RPCs, return an error. + if ctx.rpcs == noMoreRPCs { + return errors.New("attempted illegal RPC after program completion") + } + + ctx.rpcs++ + return nil +} + +// endRPC signals the completion of an RPC and notifies any potential awaiters when outstanding RPCs hit zero. +func (ctx *Context) endRPC() { + ctx.rpcsLock.Lock() + defer ctx.rpcsLock.Unlock() + + ctx.rpcs-- + if ctx.rpcs == 0 { + ctx.rpcsDone.Broadcast() + } +} + +// waitForRPCs awaits the completion of any outstanding RPCs and then leaves behind a sentinel to prevent +// any subsequent ones from starting. This is often used during the shutdown of a program to ensure no RPCs +// go missing due to the program exiting prior to their completion. +func (ctx *Context) waitForRPCs() { + ctx.rpcsLock.Lock() + defer ctx.rpcsLock.Unlock() + + // Wait until the RPC count hits zero. + for ctx.rpcs > 0 { + ctx.rpcsDone.Wait() + } + + // Mark the RPCs flag so that no more RPCs are permitted. + ctx.rpcs = noMoreRPCs +} + +// ResourceState contains the results of a resource registration operation. +type ResourceState struct { + // URN will resolve to the resource's URN after registration has completed. + URN *Output + // ID will resolve to the resource's ID after registration, provided this is for a custom resource. + ID *Output + // State contains the full set of expected output properties and will resolve after completion. + State Outputs +} + +// RegisterResourceOutputs completes the resource registration, attaching an optional set of computed outputs. +func (ctx *Context) RegisterResourceOutputs(urn URN, outs Inputs) error { + return nil +} + +// Export registers a key and value pair with the current context's stack. +func (ctx *Context) Export(name string, value Input) { + ctx.exports[name] = value +} diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go new file mode 100644 index 000000000..8162e5660 --- /dev/null +++ b/sdk/go/pulumi/properties.go @@ -0,0 +1,140 @@ +// 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 pulumi + +// Input is an input property for a resource. It is a discriminated union of either a value or another resource's +// output value, which will make the receiving resource dependent on the resource from which the output came. +type Input interface{} + +// Inputs is a map of property name to value, one for each resource input property. Each value can be a prompt, +// JSON serializable primitive -- bool, string, int, array, or map -- or it can be an *Output, in which case the +// input property will carry dependency information from the resource to which the output belongs. +type Inputs map[string]interface{} + +// Output helps encode the relationship between resources in a Pulumi application. Specifically an output property +// holds onto a value and the resource it came from. An output value can then be provided when constructing new +// resources, allowing that new resource to know both the value as well as the resource the value came from. This +// allows for a precise "dependency graph" to be created, which properly tracks the relationship between resources. +type Output struct { + sync chan *valueOrError // the channel for outputs whose values are not yet known. + voe *valueOrError // the value or error, after the channel has been rendezvoused with. + deps []Resource // the dependencies associated with this output property. +} + +// valueOrError is a discriminated union between a value (possibly nil) or an error. +type valueOrError struct { + value interface{} // a value, if the output resolved to a value. + err error // an error, if the producer yielded an error instead of a value. +} + +// NewOutput returns an output value that can be used to rendezvous with the production of a value or error. The +// function returns the output itself, plus two functions: one for resolving a value, and another for rejecting with an +// error; exactly one function must be called. This acts like a promise. +func NewOutput(deps []Resource) (*Output, func(interface{}), func(error)) { + out := &Output{ + sync: make(chan *valueOrError, 1), + deps: deps, + } + resolve := func(v interface{}) { + out.sync <- &valueOrError{value: v} + } + reject := func(err error) { + out.sync <- &valueOrError{err: err} + } + return out, resolve, reject +} + +// Apply transforms the data of the output property using the applier func. The result remains an output property, +// and accumulates all implicated dependencies, so that resources can be properly tracked using a DAG. This function +// does not block awaiting the value; instead, it spawns a Goroutine that will await its availability. +func (out *Output) Apply(applier func(v interface{}) (interface{}, error)) *Output { + result, resolve, reject := NewOutput(out.Deps()) + go func() { + for { + v, err := out.Value() + if err != nil { + reject(err) + break + } else { + // If we have a value, run the applier to transform it. + u, err := applier(v) + if err != nil { + reject(err) + break + } else { + // Now that we've transformed the value, it's possible we have another output. If so, pluck it + // out and go around to await it until we hit a real value. Note that we are not capturing the + // resources of this inner output, intentionally, as the output returned should be related to + // this output already. + if newout, ok := v.(*Output); ok { + out = newout + } else { + resolve(u) + break + } + } + } + } + }() + return result +} + +// Deps returns the dependencies for this output property. +func (out *Output) Deps() []Resource { + return out.deps +} + +// Value retrieves the underlying value for this output property. +func (out *Output) Value() (interface{}, error) { + // If neither error nor value are available, first await the channel. Only one Goroutine will make it through this + // and is responsible for closing the channel, to signal to other awaiters that it's safe to read the values. + if out.voe == nil { + if voe := <-out.sync; voe != nil { + out.voe = voe + close(out.sync) + } + } + return out.voe.value, out.voe.err +} + +// String retrives the underlying value for this output property as a string. +func (out *Output) String() (string, error) { + v, err := out.Value() + if err != nil { + return "", err + } + return v.(string), nil +} + +// ID retrives the underlying value for this output property as an ID. +func (out *Output) ID() (ID, error) { + v, err := out.Value() + if err != nil { + return "", err + } + return v.(ID), nil +} + +// URN retrives the underlying value for this output property as a URN. +func (out *Output) URN() (URN, error) { + v, err := out.Value() + if err != nil { + return "", err + } + return v.(URN), nil +} + +// Outputs is a map of property name to value, one for each resource output property. +type Outputs map[string]*Output diff --git a/sdk/go/pulumi/resource.go b/sdk/go/pulumi/resource.go new file mode 100644 index 000000000..4febaf65e --- /dev/null +++ b/sdk/go/pulumi/resource.go @@ -0,0 +1,54 @@ +// 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 pulumi + +type ( + // ID is a unique identifier assigned by a resource provider to a resource. + ID string + // URN is an automatically generated logical URN, used to stably identify resources. + URN string +) + +// Resource represents a cloud resource managed by Pulumi. +type Resource interface { + // URN is this resource's stable logical URN used to distinctly address it before, during, and after deployments. + URN() URN +} + +// CustomResource is a cloud resource whose create, read, update, and delete (CRUD) operations are managed by performing +// external operations on some physical entity. The engine understands how to diff and perform partial updates of them, +// and these CRUD operations are implemented in a dynamically loaded plugin for the defining package. +type CustomResource interface { + Resource + // ID is the provider-assigned unique identifier for this managed resource. It is set during deployments, + // but might be missing ("") during planning phases. + ID() ID +} + +// ComponentResource is a resource that aggregates one or more other child resources into a higher level abstraction. +// The component resource itself is a resource, but does not require custom CRUD operations for provisioning. +type ComponentResource interface { + Resource +} + +// ResourceOpt contains optional settings that control a resource's behavior. +type ResourceOpt struct { + // Parent is an optional parent resource to which this resource belongs. + Parent Resource + // DependsOn is an optional array of explicit dependencies on other resources. + DependsOn []Resource + // Protect, when set to true, ensures that this resource cannot be deleted (without first setting it to false). + Protect bool +} diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go new file mode 100644 index 000000000..38cb797d4 --- /dev/null +++ b/sdk/go/pulumi/rpc.go @@ -0,0 +1,175 @@ +// 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 pulumi + +import ( + "reflect" + "sort" + + structpb "github.com/golang/protobuf/ptypes/struct" + "github.com/pkg/errors" + + "github.com/pulumi/pulumi/pkg/resource" + "github.com/pulumi/pulumi/pkg/resource/plugin" + "github.com/pulumi/pulumi/sdk/go/pulumi/asset" +) + +// marshalInputs turns resource property inputs into a gRPC struct suitable for marshaling. +func marshalInputs(props Inputs) ([]string, *structpb.Struct, []URN, error) { + var keys []string + for key := range props { + keys = append(keys, key) + } + sort.Strings(keys) + + var depURNs []URN + pmap := make(map[string]interface{}) + for _, key := range keys { + // Get the underlying value, possibly waiting for an output to arrive. + v, deps, err := marshalInput(props[key]) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "awaiting input property %s", key) + } + + pmap[key] = v + + // Record all dependencies accumulated from reading this property. + for _, dep := range deps { + depURNs = append(depURNs, dep.URN()) + } + } + + // Marshal all properties for the RPC call. + m, err := plugin.MarshalProperties( + resource.NewPropertyMapFromMap(pmap), + plugin.MarshalOptions{KeepUnknowns: true}, + ) + return keys, m, depURNs, err +} + +const ( + rpcTokenSpecialSigKey = "4dabf18193072939515e22adb298388d" + rpcTokenSpecialAssetSig = "c44067f5952c0a294b673a41bacd8c17" + rpcTokenSpecialArchiveSig = "0def7320c3a5731c473e5ecbe6d01bc7" + rpcTokenUnknownValue = "04da6b54-80e4-46f7-96ec-b56ff0331ba9" +) + +// marshalInput marshals an input value, returning its raw serializable value along with any dependencies. +func marshalInput(v interface{}) (interface{}, []Resource, error) { + // If nil, just return that. + if v == nil { + return nil, nil, nil + } + + // Next, look for some well known types. + switch t := v.(type) { + case bool, int, uint, int32, uint32, int64, uint64, float32, float64, string: + return t, nil, nil + case CustomResource: + // Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a + e, d, err := marshalInput(t.ID()) + if err != nil { + return nil, nil, err + } + return e, append([]Resource{t}, d...), nil + case asset.Asset: + return map[string]interface{}{ + rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig, + "path": t.Path(), + "text": t.Text(), + "uri": t.URI(), + }, nil, nil + case asset.Archive: + var assets map[string]interface{} + if as := t.Assets(); as != nil { + assets = make(map[string]interface{}) + for k, a := range as { + aa, _, err := marshalInput(a) + if err != nil { + return nil, nil, err + } + assets[k] = aa + } + } + + return map[string]interface{}{ + rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig, + "assets": assets, + "path": t.Path(), + "uri": t.URI(), + }, nil, nil + case *Output: + // Await the value and return its raw value. + v, err := t.Value() + if err != nil { + return nil, nil, err + } + // TODO: unknownValue + e, d, err := marshalInput(v) + if err != nil { + return nil, nil, err + } + return e, append(t.Deps(), d...), err + } + + // Finally, handle the usual primitives (numbers, strings, arrays, maps, ...) + rv := reflect.ValueOf(v) + switch rk := rv.Type().Kind(); rk { + case reflect.Array, reflect.Slice: + // If an array or a slice, create a new array by recursing into elements. + var arr []interface{} + var deps []Resource + for i := 0; i < rv.Len(); i++ { + elem := rv.Index(i) + e, d, err := marshalInput(elem.Interface()) + if err != nil { + return nil, nil, err + } + arr = append(arr, e) + deps = append(deps, d...) + } + return arr, deps, nil + case reflect.Map: + // For maps, only support string-based keys, and recurse into the values. + var obj map[string]interface{} + var deps []Resource + for _, key := range rv.MapKeys() { + k, ok := key.Interface().(string) + if !ok { + return nil, nil, + errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface())) + } + value := rv.MapIndex(key) + v, d, err := marshalInput(value) + if err != nil { + return nil, nil, err + } + + obj[k] = v + deps = append(deps, d...) + } + return obj, deps, nil + case reflect.Ptr: + // For pointerss, recurse into the underlying value. + if rv.IsNil() { + return nil, nil, nil + } + return marshalInput(rv.Interface()) + case reflect.String: + return marshalInput(rv.String()) + } + + return nil, nil, errors.Errorf("unrecognized input property type: %v", reflect.TypeOf(v)) +} diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go new file mode 100644 index 000000000..af2a0c932 --- /dev/null +++ b/sdk/go/pulumi/run.go @@ -0,0 +1,133 @@ +// 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 pulumi + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + + "github.com/hashicorp/go-multierror" + "golang.org/x/net/context" +) + +// Run executes the body of a Pulumi program, granting it access to a deployment context that it may use +// to register resources and orchestrate deployment activities. This connects back to the Pulumi engine using gRPC. +// If the program fails, the process will be terminated and the function will not return. +func Run(body RunFunc) { + if err := RunErr(body); err != nil { + fmt.Fprintf(os.Stderr, "error: program failed: %v\n", err) + os.Exit(1) + } +} + +// Run executes the body of a Pulumi program, granting it access to a deployment context that it may use +// to register resources and orchestrate deployment activities. This connects back to the Pulumi engine using gRPC. +func RunErr(body RunFunc) error { + // Parse the info out of environment variables. This is a lame contract with the caller, but helps to keep + // boilerplate to a minimum in the average Pulumi Go program. + // TODO(joe): this is a fine default, but consider `...RunOpt`s to control how we get the various addresses, etc. + info := getEnvInfo() + + // Create a fresh context. + ctx, err := NewContext(context.TODO(), info) + if err != nil { + return err + } + defer ctx.Close() + + // Create a root stack resource that we'll parent everything to. + reg, err := ctx.RegisterResource( + "pulumi:pulumi:Stack", fmt.Sprintf("%s-%s", info.Project, info.Stack), false, nil) + if err != nil { + return err + } + ctx.stackR, err = reg.URN.URN() + if err != nil { + return err + } + + // Execute the body. + var result error + if err = body(ctx); err != nil { + result = multierror.Append(result, err) + } + + // Ensure all outstanding RPCs have completed before proceeding. Also, prevent any new RPCs from happening. + ctx.waitForRPCs() + + // Register all the outputs to the stack object. + if err = ctx.RegisterResourceOutputs(ctx.stackR, ctx.exports); err != nil { + result = multierror.Append(result, err) + } + + // Propagate the error from the body, if any. + return err +} + +// RunFunc executes the body of a Pulumi program. It may register resources using the deployment context +// supplied as an arguent and any non-nil return value is interpreted as a program error by the Pulumi runtime. +type RunFunc func(ctx *Context) error + +// RunInfo contains all the metadata about a run request. +type RunInfo struct { + Project string + Stack string + Config map[string]string + Parallel int + DryRun bool + MonitorAddr string + EngineAddr string +} + +// getEnvInfo reads various program information from the process environment. +func getEnvInfo() RunInfo { + // Most of the variables are just strings, and we can read them directly. A few of them require more parsing. + parallel, _ := strconv.Atoi(os.Getenv(EnvParallel)) + dryRun, _ := strconv.ParseBool(os.Getenv(EnvDryRun)) + + var config map[string]string + if cfg := os.Getenv(EnvConfig); cfg != "" { + _ = json.Unmarshal([]byte(cfg), &config) + } + + return RunInfo{ + Project: os.Getenv(EnvProject), + Stack: os.Getenv(EnvStack), + Config: config, + Parallel: parallel, + DryRun: dryRun, + MonitorAddr: os.Getenv(EnvMonitor), + EngineAddr: os.Getenv(EnvEngine), + } +} + +const ( + // EnvProject is the envvar used to read the current Pulumi project name. + EnvProject = "PULUMI_PROJECT" + // EnvStack is the envvar used to read the current Pulumi stack name. + EnvStack = "PULUMI_STACK" + // EnvConfig is the envvar used to read the current Pulumi configuration variables. + EnvConfig = "PULUMI_CONFIG" + // EnvParallel is the envvar used to read the current Pulumi degree of parallelism. + EnvParallel = "PULUMI_PARALLEL" + // EnvDryRun is the envvar used to read the current Pulumi dry-run setting. + EnvDryRun = "PULUMI_DRY_RUN" + // EnvMonitor is the envvar used to read the current Pulumi monitor RPC address. + EnvMonitor = "PULUMI_MONITOR" + // EnvEngine is the envvar used to read the current Pulumi engine RPC address. + EnvEngine = "PULUMI_ENGINE" +) From 74a896bb7afb62638e96cab88aaaa7b576422637 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Fri, 8 Jun 2018 09:17:12 -0700 Subject: [PATCH 02/19] Add strongly typed outputs This change adds some convenience functions and types, to make strongly typed outputs more pleasant to interact with. It also includes tests for output generally, in addition to these new functions and types. --- pkg/backend/snapshot_test.go | 3 +- sdk/go/pulumi/properties.go | 66 +++++++++++++ sdk/go/pulumi/properties_test.go | 155 +++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 sdk/go/pulumi/properties_test.go diff --git a/pkg/backend/snapshot_test.go b/pkg/backend/snapshot_test.go index 8ca3e53e9..f514d6056 100644 --- a/pkg/backend/snapshot_test.go +++ b/pkg/backend/snapshot_test.go @@ -18,11 +18,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/pulumi/pulumi/pkg/resource" "github.com/pulumi/pulumi/pkg/resource/deploy" "github.com/pulumi/pulumi/pkg/tokens" "github.com/pulumi/pulumi/pkg/version" - "github.com/stretchr/testify/assert" ) type MockRegisterResourceEvent struct { diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 8162e5660..070bcbdd7 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -109,6 +109,42 @@ func (out *Output) Value() (interface{}, error) { return out.voe.value, out.voe.err } +// Array retrives the underlying value for this output property as an array. +func (out *Output) Array() ([]interface{}, error) { + v, err := out.Value() + if err != nil { + return nil, err + } + return v.([]interface{}), nil +} + +// Bool retrives the underlying value for this output property as a bool. +func (out *Output) Bool() (bool, error) { + v, err := out.Value() + if err != nil { + return false, err + } + return v.(bool), nil +} + +// Map retrives the underlying value for this output property as a map. +func (out *Output) Map() (map[string]interface{}, error) { + v, err := out.Value() + if err != nil { + return nil, err + } + return v.(map[string]interface{}), nil +} + +// Number retrives the underlying value for this output property as a number. +func (out *Output) Number() (float64, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return v.(float64), nil +} + // String retrives the underlying value for this output property as a string. func (out *Output) String() (string, error) { v, err := out.Value() @@ -138,3 +174,33 @@ func (out *Output) URN() (URN, error) { // Outputs is a map of property name to value, one for each resource output property. type Outputs map[string]*Output + +// ArrayOutput is an Output that is typed to return arrays of values. +type ArrayOutput Output + +// Value returns the underlying array value. +func (out *ArrayOutput) Value() ([]interface{}, error) { return (*Output)(out).Array() } + +// BoolOutput is an Output that is typed to return bool values. +type BoolOutput Output + +// Value returns the underlying bool value. +func (out *BoolOutput) Value() (bool, error) { return (*Output)(out).Bool() } + +// MapOutput is an Output that is typed to return string-keyed maps of values. +type MapOutput Output + +// Value returns the underlying map value. +func (out *MapOutput) Value() (map[string]interface{}, error) { return (*Output)(out).Map() } + +// NumberOutput is an Output that is typed to return number values. +type NumberOutput Output + +// Value returns the underlying number value. +func (out *NumberOutput) Value() (float64, error) { return (*Output)(out).Number() } + +// StringOutput is an Output that is typed to return number values. +type StringOutput Output + +// Value returns the underlying number value. +func (out *StringOutput) Value() (string, error) { return (*Output)(out).String() } diff --git a/sdk/go/pulumi/properties_test.go b/sdk/go/pulumi/properties_test.go new file mode 100644 index 000000000..41ebee2fa --- /dev/null +++ b/sdk/go/pulumi/properties_test.go @@ -0,0 +1,155 @@ +// 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 pulumi + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestBasicOutputs(t *testing.T) { + // Just test basic resolve and reject functionality. + { + out, resolve, _ := NewOutput(nil) + go func() { + resolve(42) + }() + v, err := out.Value() + assert.Nil(t, err) + assert.NotNil(t, v) + assert.Equal(t, 42, v.(int)) + } + { + out, _, reject := NewOutput(nil) + go func() { + reject(errors.New("boom")) + }() + v, err := out.Value() + assert.NotNil(t, err) + assert.Nil(t, v) + } +} + +func TestArrayOutputs(t *testing.T) { + out, resolve, _ := NewOutput(nil) + go func() { + resolve([]interface{}{nil, 0, "x"}) + }() + { + v, err := out.Array() + assert.Nil(t, err) + assert.NotNil(t, v) + if assert.Equal(t, 3, len(v)) { + assert.Equal(t, nil, v[0]) + assert.Equal(t, 0, v[1]) + assert.Equal(t, "x", v[2]) + } + } + { + arr := (*ArrayOutput)(out) + v, err := arr.Value() + assert.Nil(t, err) + assert.NotNil(t, v) + if assert.Equal(t, 3, len(v)) { + assert.Equal(t, nil, v[0]) + assert.Equal(t, 0, v[1]) + assert.Equal(t, "x", v[2]) + } + } +} + +func TestBoolOutputs(t *testing.T) { + out, resolve, _ := NewOutput(nil) + go func() { + resolve(true) + }() + { + v, err := out.Bool() + assert.Nil(t, err) + assert.True(t, v) + } + { + b := (*BoolOutput)(out) + v, err := b.Value() + assert.Nil(t, err) + assert.True(t, v) + } +} + +func TestMapOutputs(t *testing.T) { + out, resolve, _ := NewOutput(nil) + go func() { + resolve(map[string]interface{}{ + "x": 1, + "y": false, + "z": "abc", + }) + }() + { + v, err := out.Map() + assert.Nil(t, err) + assert.NotNil(t, v) + assert.Equal(t, 1, v["x"]) + assert.Equal(t, false, v["y"]) + assert.Equal(t, "abc", v["z"]) + } + { + b := (*MapOutput)(out) + v, err := b.Value() + assert.Nil(t, err) + assert.NotNil(t, v) + assert.Equal(t, 1, v["x"]) + assert.Equal(t, false, v["y"]) + assert.Equal(t, "abc", v["z"]) + } +} + +func TestNumberOutputs(t *testing.T) { + out, resolve, _ := NewOutput(nil) + go func() { + resolve(42.345) + }() + { + v, err := out.Number() + assert.Nil(t, err) + assert.Equal(t, 42.345, v) + } + { + b := (*NumberOutput)(out) + v, err := b.Value() + assert.Nil(t, err) + assert.Equal(t, 42.345, v) + } +} + +func TestStringOutputs(t *testing.T) { + out, resolve, _ := NewOutput(nil) + go func() { + resolve("a stringy output") + }() + { + v, err := out.String() + assert.Nil(t, err) + assert.Equal(t, "a stringy output", v) + } + { + b := (*StringOutput)(out) + v, err := b.Value() + assert.Nil(t, err) + assert.Equal(t, "a stringy output", v) + } +} From c3b13348d0f2d10d0171a7b0cd4378867f12f3a1 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Fri, 8 Jun 2018 12:57:59 -0700 Subject: [PATCH 03/19] Improve strong typing This improves the strong typing of output properties, by leveraging the cast library to support numeric conversions to and from many types, without hitting interface{}-cast panics. Also adds strongly typed applies and adds a number of additional tests for these functions. --- Gopkg.lock | 8 +- sdk/go/pulumi/properties.go | 421 +++++++++++++++++++++++++++++-- sdk/go/pulumi/properties_test.go | 98 ++++++- 3 files changed, 498 insertions(+), 29 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 36eba18b9..fce8c0f82 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -306,6 +306,12 @@ packages = ["open"] revision = "75fb7ed4208cf72d323d7d02fd1a5964a7a9073c" +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" + [[projects]] branch = "master" name = "github.com/spf13/cobra" @@ -554,6 +560,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a3c19b57bc10408ea6f35080bdca4a22257e7ab588c1fe3dfc88cf2b00aeea52" + inputs-digest = "f974a423ae8de19a1ba9f68115b5ab111040baece859d32f89e885094d3ebd86" solver-name = "gps-cdcl" solver-version = 1 diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 070bcbdd7..a0129238e 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -14,6 +14,14 @@ package pulumi +import ( + "time" + + "github.com/spf13/cast" + + "github.com/pulumi/pulumi/sdk/go/pulumi/asset" +) + // Input is an input property for a resource. It is a discriminated union of either a value or another resource's // output value, which will make the receiving resource dependent on the resource from which the output came. type Input interface{} @@ -47,13 +55,31 @@ func NewOutput(deps []Resource) (*Output, func(interface{}), func(error)) { sync: make(chan *valueOrError, 1), deps: deps, } - resolve := func(v interface{}) { + return out, out.resolve, out.reject +} + +// resolve will resolve the output. It is not exported, because we want to control the capabilities tightly, such +// that anybody who happens to have an Output is not allowed to resolve it; only those who created it can. +func (out *Output) resolve(v interface{}) { + // If v is another output, chain this rather than resolving to an output directly. + if other, isOut := v.(*Output); isOut { + go func() { + real, err := other.Value() + if err != nil { + out.reject(err) + } else { + out.resolve(real) + } + }() + } else { out.sync <- &valueOrError{value: v} } - reject := func(err error) { - out.sync <- &valueOrError{err: err} - } - return out, resolve, reject +} + +// reject will reject the output. It is not exported, because we want to control the capabilities tightly, such +// that anybody who happens to have an Output is not allowed to reject it; only those who created it can. +func (out *Output) reject(err error) { + out.sync <- &valueOrError{err: err} } // Apply transforms the data of the output property using the applier func. The result remains an output property, @@ -109,22 +135,40 @@ func (out *Output) Value() (interface{}, error) { return out.voe.value, out.voe.err } +// Archive retrives the underlying value for this output property as an archive. +func (out *Output) Archive() (asset.Archive, error) { + v, err := out.Value() + if err != nil { + return nil, err + } + return v.(asset.Archive), nil +} + // Array retrives the underlying value for this output property as an array. func (out *Output) Array() ([]interface{}, error) { v, err := out.Value() if err != nil { return nil, err } - return v.([]interface{}), nil + return cast.ToSlice(v), nil } -// Bool retrives the underlying value for this output property as a bool. +// Asset retrives the underlying value for this output property as an asset. +func (out *Output) Asset() (asset.Asset, error) { + v, err := out.Value() + if err != nil { + return nil, err + } + return v.(asset.Asset), nil +} + +/// Bool retrives the underlying value for this output property as a bool. func (out *Output) Bool() (bool, error) { v, err := out.Value() if err != nil { return false, err } - return v.(bool), nil + return cast.ToBool(v), nil } // Map retrives the underlying value for this output property as a map. @@ -133,25 +177,25 @@ func (out *Output) Map() (map[string]interface{}, error) { if err != nil { return nil, err } - return v.(map[string]interface{}), nil + return cast.ToStringMap(v), nil } -// Number retrives the underlying value for this output property as a number. -func (out *Output) Number() (float64, error) { +// Float32 retrives the underlying value for this output property as a float32. +func (out *Output) Float32() (float32, error) { v, err := out.Value() if err != nil { return 0, err } - return v.(float64), nil + return cast.ToFloat32(v), nil } -// String retrives the underlying value for this output property as a string. -func (out *Output) String() (string, error) { +// Float64 retrives the underlying value for this output property as a float64. +func (out *Output) Float64() (float64, error) { v, err := out.Value() if err != nil { - return "", err + return 0, err } - return v.(string), nil + return cast.ToFloat64(v), nil } // ID retrives the underlying value for this output property as an ID. @@ -160,7 +204,115 @@ func (out *Output) ID() (ID, error) { if err != nil { return "", err } - return v.(ID), nil + return ID(cast.ToString(v)), nil +} + +// Int retrives the underlying value for this output property as a int. +func (out *Output) Int() (int, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToInt(v), nil +} + +// Int8 retrives the underlying value for this output property as a int8. +func (out *Output) Int8() (int8, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToInt8(v), nil +} + +// Int16 retrives the underlying value for this output property as a int16. +func (out *Output) Int16() (int16, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToInt16(v), nil +} + +// Int32 retrives the underlying value for this output property as a int32. +func (out *Output) Int32() (int32, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToInt32(v), nil +} + +// Int64 retrives the underlying value for this output property as a int64. +func (out *Output) Int64() (int64, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToInt64(v), nil +} + +// String retrives the underlying value for this output property as a string. +func (out *Output) String() (string, error) { + v, err := out.Value() + if err != nil { + return "", err + } + return cast.ToString(v), nil +} + +// Time retrives the underlying value for this output property as a time. +func (out *Output) Time() (time.Time, error) { + v, err := out.Value() + if err != nil { + return time.Time{}, err + } + return cast.ToTime(v), nil +} + +// Uuint retrives the underlying value for this output property as a uint. +func (out *Output) Uint() (uint, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToUint(v), nil +} + +// Uuint8 retrives the underlying value for this output property as a uint8. +func (out *Output) Uint8() (uint8, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToUint8(v), nil +} + +// Uuint16 retrives the underlying value for this output property as a uint16. +func (out *Output) Uint16() (uint16, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToUint16(v), nil +} + +// Uuint32 retrives the underlying value for this output property as a uint32. +func (out *Output) Uint32() (uint32, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToUint32(v), nil +} + +// Uuint64 retrives the underlying value for this output property as a uint64. +func (out *Output) Uint64() (uint64, error) { + v, err := out.Value() + if err != nil { + return 0, err + } + return cast.ToUint64(v), nil } // URN retrives the underlying value for this output property as a URN. @@ -169,38 +321,255 @@ func (out *Output) URN() (URN, error) { if err != nil { return "", err } - return v.(URN), nil + return URN(cast.ToString(v)), nil } // Outputs is a map of property name to value, one for each resource output property. type Outputs map[string]*Output +// ArchiveOutput is an Output that is typed to return archive values. +type ArchiveOutput Output + +// Value returns the underlying archive value. +func (out *ArchiveOutput) Value() (asset.Archive, error) { return (*Output)(out).Archive() } + +// Apply applies a transformation to the archive value when it is available. +func (out *ArchiveOutput) Apply(applier func(asset.Archive) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(v.(asset.Archive)) + }) +} + // ArrayOutput is an Output that is typed to return arrays of values. type ArrayOutput Output // Value returns the underlying array value. func (out *ArrayOutput) Value() ([]interface{}, error) { return (*Output)(out).Array() } +// Apply applies a transformation to the array value when it is available. +func (out *ArrayOutput) Apply(applier func([]interface{}) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToSlice(v)) + }) +} + +// AssetOutput is an Output that is typed to return asset values. +type AssetOutput Output + +// Value returns the underlying asset value. +func (out *AssetOutput) Value() (asset.Asset, error) { return (*Output)(out).Asset() } + +// Apply applies a transformation to the asset value when it is available. +func (out *AssetOutput) Apply(applier func(asset.Asset) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(v.(asset.Asset)) + }) +} + // BoolOutput is an Output that is typed to return bool values. type BoolOutput Output // Value returns the underlying bool value. func (out *BoolOutput) Value() (bool, error) { return (*Output)(out).Bool() } -// MapOutput is an Output that is typed to return string-keyed maps of values. -type MapOutput Output +// Apply applies a transformation to the bool value when it is available. +func (out *BoolOutput) Apply(applier func(bool) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(v.(bool)) + }) +} -// Value returns the underlying map value. -func (out *MapOutput) Value() (map[string]interface{}, error) { return (*Output)(out).Map() } - -// NumberOutput is an Output that is typed to return number values. -type NumberOutput Output +// Float32Output is an Output that is typed to return float32 values. +type Float32Output Output // Value returns the underlying number value. -func (out *NumberOutput) Value() (float64, error) { return (*Output)(out).Number() } +func (out *Float32Output) Value() (float32, error) { return (*Output)(out).Float32() } + +// Apply applies a transformation to the float32 value when it is available. +func (out *Float32Output) Apply(applier func(float32) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToFloat32(v)) + }) +} + +// Float64Output is an Output that is typed to return float64 values. +type Float64Output Output + +// Value returns the underlying number value. +func (out *Float64Output) Value() (float64, error) { return (*Output)(out).Float64() } + +// Apply applies a transformation to the float64 value when it is available. +func (out *Float64Output) Apply(applier func(float64) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToFloat64(v)) + }) +} + +// IntOutput is an Output that is typed to return int values. +type IntOutput Output + +// Value returns the underlying number value. +func (out *IntOutput) Value() (int, error) { return (*Output)(out).Int() } + +// Apply applies a transformation to the int value when it is available. +func (out *IntOutput) Apply(applier func(int) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToInt(v)) + }) +} + +// Int8Output is an Output that is typed to return int8 values. +type Int8Output Output + +// Value returns the underlying number value. +func (out *Int8Output) Value() (int8, error) { return (*Output)(out).Int8() } + +// Apply applies a transformation to the int8 value when it is available. +func (out *Int8Output) Apply(applier func(int8) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToInt8(v)) + }) +} + +// Int16Output is an Output that is typed to return int16 values. +type Int16Output Output + +// Value returns the underlying number value. +func (out *Int16Output) Value() (int16, error) { return (*Output)(out).Int16() } + +// Apply applies a transformation to the int16 value when it is available. +func (out *Int16Output) Apply(applier func(int16) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToInt16(v)) + }) +} + +// Int32Output is an Output that is typed to return int32 values. +type Int32Output Output + +// Value returns the underlying number value. +func (out *Int32Output) Value() (int32, error) { return (*Output)(out).Int32() } + +// Apply applies a transformation to the int32 value when it is available. +func (out *Int32Output) Apply(applier func(int32) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToInt32(v)) + }) +} + +// Int64Output is an Output that is typed to return int64 values. +type Int64Output Output + +// Value returns the underlying number value. +func (out *Int64Output) Value() (int64, error) { return (*Output)(out).Int64() } + +// Apply applies a transformation to the int64 value when it is available. +func (out *Int64Output) Apply(applier func(int64) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToInt64(v)) + }) +} + +// MapOutput is an Output that is typed to return map values. +type MapOutput Output + +// Value returns the underlying number value. +func (out *MapOutput) Value() (map[string]interface{}, error) { return (*Output)(out).Map() } + +// Apply applies a transformation to the number value when it is available. +func (out *MapOutput) Apply(applier func(map[string]interface{}) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToStringMap(v)) + }) +} // StringOutput is an Output that is typed to return number values. type StringOutput Output // Value returns the underlying number value. func (out *StringOutput) Value() (string, error) { return (*Output)(out).String() } + +// Apply applies a transformation to the number value when it is available. +func (out *StringOutput) Apply(applier func(string) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToString(v)) + }) +} + +// TimeOutput is an Output that is typed to return number values. +type TimeOutput Output + +// Value returns the underlying number value. +func (out *TimeOutput) Value() (time.Time, error) { return (*Output)(out).Time() } + +// Apply applies a transformation to the number value when it is available. +func (out *TimeOutput) Apply(applier func(time.Time) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToTime(v)) + }) +} + +// UintOutput is an Output that is typed to return uint values. +type UintOutput Output + +// Value returns the underlying number value. +func (out *UintOutput) Value() (uint, error) { return (*Output)(out).Uint() } + +// Apply applies a transformation to the uint value when it is available. +func (out *UintOutput) Apply(applier func(uint) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToUint(v)) + }) +} + +// Uint8Output is an Output that is typed to return uint8 values. +type Uint8Output Output + +// Value returns the underlying number value. +func (out *Uint8Output) Value() (uint8, error) { return (*Output)(out).Uint8() } + +// Apply applies a transformation to the uint8 value when it is available. +func (out *Uint8Output) Apply(applier func(uint8) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToUint8(v)) + }) +} + +// Uint16Output is an Output that is typed to return uint16 values. +type Uint16Output Output + +// Value returns the underlying number value. +func (out *Uint16Output) Value() (uint16, error) { return (*Output)(out).Uint16() } + +// Apply applies a transformation to the uint16 value when it is available. +func (out *Uint16Output) Apply(applier func(uint16) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToUint16(v)) + }) +} + +// Uint32Output is an Output that is typed to return uint32 values. +type Uint32Output Output + +// Value returns the underlying number value. +func (out *Uint32Output) Value() (uint32, error) { return (*Output)(out).Uint32() } + +// Apply applies a transformation to the uint32 value when it is available. +func (out *Uint32Output) Apply(applier func(uint32) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToUint32(v)) + }) +} + +// Uint64Output is an Output that is typed to return uint64 values. +type Uint64Output Output + +// Value returns the underlying number value. +func (out *Uint64Output) Value() (uint64, error) { return (*Output)(out).Uint64() } + +// Apply applies a transformation to the uint64 value when it is available. +func (out *Uint64Output) Apply(applier func(uint64) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToUint64(v)) + }) +} diff --git a/sdk/go/pulumi/properties_test.go b/sdk/go/pulumi/properties_test.go index 41ebee2fa..f0222c25e 100644 --- a/sdk/go/pulumi/properties_test.go +++ b/sdk/go/pulumi/properties_test.go @@ -124,12 +124,12 @@ func TestNumberOutputs(t *testing.T) { resolve(42.345) }() { - v, err := out.Number() + v, err := out.Float64() assert.Nil(t, err) assert.Equal(t, 42.345, v) } { - b := (*NumberOutput)(out) + b := (*Float64Output)(out) v, err := b.Value() assert.Nil(t, err) assert.Equal(t, 42.345, v) @@ -153,3 +153,97 @@ func TestStringOutputs(t *testing.T) { assert.Equal(t, "a stringy output", v) } } + +func TestResolveOutputToOutput(t *testing.T) { + // Test that resolving an output to an output yields the value, not the output. + { + out, resolve, _ := NewOutput(nil) + go func() { + other, resolveOther, _ := NewOutput(nil) + resolve(other) + go func() { resolveOther(99) }() + }() + v, err := out.Value() + assert.Nil(t, err) + assert.Equal(t, v, 99) + } + // Similarly, test that resolving an output to a rejected output yields an error. + { + out, resolve, _ := NewOutput(nil) + go func() { + other, _, rejectOther := NewOutput(nil) + resolve(other) + go func() { rejectOther(errors.New("boom")) }() + }() + v, err := out.Value() + assert.NotNil(t, err) + assert.Nil(t, v) + } +} + +func TestOutputApply(t *testing.T) { + // Test that resolved outputs lead to applies being run. + { + out, resolve, _ := NewOutput(nil) + go func() { resolve(42) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + ranApp = true + return v + 1, nil + }) + v, err := app.Value() + assert.True(t, ranApp) + assert.Nil(t, err) + assert.Equal(t, v, 43) + } + // Test that rejected outputs do not run the apply, and instead flow the error. + { + out, _, reject := NewOutput(nil) + go func() { reject(errors.New("boom")) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + ranApp = true + return v + 1, nil + }) + v, err := app.Value() + assert.False(t, ranApp) + assert.NotNil(t, err) + assert.Nil(t, v) + } + // Test that an an apply that returns an output returns the resolution of that output, not the output itself. + { + out, resolve, _ := NewOutput(nil) + go func() { resolve(42) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + other, resolveOther, _ := NewOutput(nil) + go func() { resolveOther(v + 1) }() + ranApp = true + return other, nil + }) + v, err := app.Value() + assert.True(t, ranApp) + assert.Nil(t, err) + assert.Equal(t, v, 43) + } + // Test that an an apply that reject an output returns the rejection of that output, not the output itself. + { + out, resolve, _ := NewOutput(nil) + go func() { resolve(42) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + other, _, rejectOther := NewOutput(nil) + go func() { rejectOther(errors.New("boom")) }() + ranApp = true + return other, nil + }) + v, err := app.Value() + assert.True(t, ranApp) + assert.NotNil(t, err) + assert.Nil(t, v) + } +} From 2e8bbcc9ddc2e2fdba3590450afbe20f656d0d77 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 9 Jun 2018 09:11:35 -0700 Subject: [PATCH 04/19] 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. --- sdk/go/pulumi-language-go/main.go | 1 - sdk/go/pulumi/asset/asset.go | 10 +-- sdk/go/pulumi/context.go | 22 +++--- sdk/go/pulumi/properties.go | 33 --------- sdk/go/pulumi/rpc.go | 115 ++++++++++++++++++++++++++---- sdk/go/pulumi/rpc_test.go | 88 +++++++++++++++++++++++ 6 files changed, 208 insertions(+), 61 deletions(-) create mode 100644 sdk/go/pulumi/rpc_test.go diff --git a/sdk/go/pulumi-language-go/main.go b/sdk/go/pulumi-language-go/main.go index 325ddb7e0..7c3de3369 100644 --- a/sdk/go/pulumi-language-go/main.go +++ b/sdk/go/pulumi-language-go/main.go @@ -93,7 +93,6 @@ func (host *goLanguageHost) GetRequiredPlugins(ctx context.Context, // 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) diff --git a/sdk/go/pulumi/asset/asset.go b/sdk/go/pulumi/asset/asset.go index 02da0b1a4..e544dd2f2 100644 --- a/sdk/go/pulumi/asset/asset.go +++ b/sdk/go/pulumi/asset/asset.go @@ -72,21 +72,21 @@ type archive struct { // NewAssetArchive creates a new archive from an in-memory collection of named assets or other archives. func NewAssetArchive(assets map[string]interface{}) Archive { for k, a := range assets { - if _, ok := a.(*Asset); !ok { - if _, ok2 := a.(*Archive); !ok2 { + if _, ok := a.(Asset); !ok { + if _, ok2 := a.(Archive); !ok2 { contract.Failf( - "expected asset map to contain *Assets and/or *Archives; %s is %v", k, reflect.TypeOf(a)) + "expected asset map to contain Assets and/or Archives; %s is %v", k, reflect.TypeOf(a)) } } } return &archive{assets: assets} } -func NewPathArchive(path string) Archive { +func NewFileArchive(path string) Archive { return &archive{path: path} } -func NewURIArchive(uri string) Archive { +func NewRemoteArchive(uri string) Archive { return &archive{uri: uri} } diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index 16f32cdac..e9274904a 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -33,7 +33,7 @@ type Context struct { ctx context.Context info RunInfo stackR URN - exports Inputs + exports map[string]interface{} monitor pulumirpc.ResourceMonitorClient monitorConn *grpc.ClientConn engine pulumirpc.EngineClient @@ -73,7 +73,7 @@ func NewContext(ctx context.Context, info RunInfo) (*Context, error) { return &Context{ ctx: ctx, info: info, - exports: make(Inputs), + exports: make(map[string]interface{}), monitorConn: monitorConn, monitor: pulumirpc.NewResourceMonitorClient(monitorConn), engineConn: engineConn, @@ -108,7 +108,7 @@ func (ctx *Context) Parallel() int { return ctx.info.Parallel } func (ctx *Context) DryRun() bool { return ctx.info.DryRun } // Invoke will invoke a provider's function, identified by its token tok. -func (ctx *Context) Invoke(tok string, args Inputs) (Outputs, error) { +func (ctx *Context) Invoke(tok string, args map[string]interface{}) (Outputs, error) { // TODO(joe): implement this. return nil, errors.New("Invoke not yet implemented") } @@ -116,7 +116,7 @@ func (ctx *Context) Invoke(tok string, args Inputs) (Outputs, error) { // ReadResource reads an existing custom resource's state from the resource monitor. Note that resources read in this // way will not be part of the resulting stack's state, as they are presumed to belong to another. func (ctx *Context) ReadResource( - t, name string, id ID, state Inputs, opts ...ResourceOpt) (*ResourceState, error) { + t, name string, id ID, state map[string]interface{}, opts ...ResourceOpt) (*ResourceState, error) { if t == "" { return nil, errors.New("resource type argument cannot be empty") } else if name == "" { @@ -132,7 +132,7 @@ func (ctx *Context) ReadResource( // the "name" part to use in creating a stable and globally unique URN for the object. state contains the goal state // for the resource object and opts contains optional settings that govern the way the resource is created. func (ctx *Context) RegisterResource( - t, name string, custom bool, props Inputs, opts ...ResourceOpt) (*ResourceState, error) { + t, name string, custom bool, props map[string]interface{}, opts ...ResourceOpt) (*ResourceState, error) { if t == "" { return nil, errors.New("resource type argument cannot be empty") } else if name == "" { @@ -218,8 +218,12 @@ func (ctx *Context) RegisterResource( resolveID(ID(resp.Id)) } for _, key := range keys { - // TODO(joe): check for missing keys, etc. - resolveState[key](outprops[key]) + out, err := unmarshalOutput(outprops[key]) + if err != nil { + rejectState[key](err) + } else { + resolveState[key](out) + } } } @@ -329,11 +333,11 @@ type ResourceState struct { } // RegisterResourceOutputs completes the resource registration, attaching an optional set of computed outputs. -func (ctx *Context) RegisterResourceOutputs(urn URN, outs Inputs) error { +func (ctx *Context) RegisterResourceOutputs(urn URN, outs map[string]interface{}) error { return nil } // Export registers a key and value pair with the current context's stack. -func (ctx *Context) Export(name string, value Input) { +func (ctx *Context) Export(name string, value interface{}) { ctx.exports[name] = value } diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index a0129238e..92f5d8091 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -15,22 +15,11 @@ package pulumi import ( - "time" - "github.com/spf13/cast" "github.com/pulumi/pulumi/sdk/go/pulumi/asset" ) -// Input is an input property for a resource. It is a discriminated union of either a value or another resource's -// output value, which will make the receiving resource dependent on the resource from which the output came. -type Input interface{} - -// Inputs is a map of property name to value, one for each resource input property. Each value can be a prompt, -// JSON serializable primitive -- bool, string, int, array, or map -- or it can be an *Output, in which case the -// input property will carry dependency information from the resource to which the output belongs. -type Inputs map[string]interface{} - // Output helps encode the relationship between resources in a Pulumi application. Specifically an output property // holds onto a value and the resource it came from. An output value can then be provided when constructing new // resources, allowing that new resource to know both the value as well as the resource the value came from. This @@ -261,15 +250,6 @@ func (out *Output) String() (string, error) { return cast.ToString(v), nil } -// Time retrives the underlying value for this output property as a time. -func (out *Output) Time() (time.Time, error) { - v, err := out.Value() - if err != nil { - return time.Time{}, err - } - return cast.ToTime(v), nil -} - // Uuint retrives the underlying value for this output property as a uint. func (out *Output) Uint() (uint, error) { v, err := out.Value() @@ -496,19 +476,6 @@ func (out *StringOutput) Apply(applier func(string) (interface{}, error)) *Outpu }) } -// TimeOutput is an Output that is typed to return number values. -type TimeOutput Output - -// Value returns the underlying number value. -func (out *TimeOutput) Value() (time.Time, error) { return (*Output)(out).Time() } - -// Apply applies a transformation to the number value when it is available. -func (out *TimeOutput) Apply(applier func(time.Time) (interface{}, error)) *Output { - return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { - return applier(cast.ToTime(v)) - }) -} - // UintOutput is an Output that is typed to return uint values. type UintOutput Output diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go index 38cb797d4..bfdacee71 100644 --- a/sdk/go/pulumi/rpc.go +++ b/sdk/go/pulumi/rpc.go @@ -20,6 +20,7 @@ import ( structpb "github.com/golang/protobuf/ptypes/struct" "github.com/pkg/errors" + "github.com/spf13/cast" "github.com/pulumi/pulumi/pkg/resource" "github.com/pulumi/pulumi/pkg/resource/plugin" @@ -27,7 +28,7 @@ import ( ) // marshalInputs turns resource property inputs into a gRPC struct suitable for marshaling. -func marshalInputs(props Inputs) ([]string, *structpb.Struct, []URN, error) { +func marshalInputs(props map[string]interface{}) ([]string, *structpb.Struct, []URN, error) { var keys []string for key := range props { keys = append(keys, key) @@ -75,15 +76,8 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { // Next, look for some well known types. switch t := v.(type) { - case bool, int, uint, int32, uint32, int64, uint64, float32, float64, string: + case bool, int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64, string: return t, nil, nil - case CustomResource: - // Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a - e, d, err := marshalInput(t.ID()) - if err != nil { - return nil, nil, err - } - return e, append([]Resource{t}, d...), nil case asset.Asset: return map[string]interface{}{ rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig, @@ -122,6 +116,13 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { return nil, nil, err } return e, append(t.Deps(), d...), err + case CustomResource: + // Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a + e, d, err := marshalInput(t.ID()) + if err != nil { + return nil, nil, err + } + return e, append([]Resource{t}, d...), nil } // Finally, handle the usual primitives (numbers, strings, arrays, maps, ...) @@ -143,7 +144,7 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { return arr, deps, nil case reflect.Map: // For maps, only support string-based keys, and recurse into the values. - var obj map[string]interface{} + obj := make(map[string]interface{}) var deps []Resource for _, key := range rv.MapKeys() { k, ok := key.Interface().(string) @@ -152,7 +153,7 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface())) } value := rv.MapIndex(key) - v, d, err := marshalInput(value) + v, d, err := marshalInput(value.Interface()) if err != nil { return nil, nil, err } @@ -162,7 +163,7 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { } return obj, deps, nil case reflect.Ptr: - // For pointerss, recurse into the underlying value. + // For pointers, recurse into the underlying value. if rv.IsNil() { return nil, nil, nil } @@ -171,5 +172,93 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { return marshalInput(rv.String()) } - return nil, nil, errors.Errorf("unrecognized input property type: %v", reflect.TypeOf(v)) + return nil, nil, errors.Errorf("unrecognized input property type: %v (%v)", v, reflect.TypeOf(v)) +} + +// unmarshalOutputs unmarshals all the outputs into a simple map. +func unmarshalOutputs(outs *structpb.Struct) (map[string]interface{}, error) { + outprops, err := plugin.UnmarshalProperties(outs, plugin.MarshalOptions{}) + if err != nil { + return nil, err + } + + result := make(map[string]interface{}) + for k, v := range outprops.Mappable() { + result[k], err = unmarshalOutput(v) + if err != nil { + return nil, err + } + } + return result, nil +} + +// unmarshalOutput unmarshals a single output variable into its runtime representation. For the most part, this just +// returns the raw value. In a small number of cases, we need to change a type. +func unmarshalOutput(v interface{}) (interface{}, error) { + // In the case of assets and archives, turn these into real asset and archive structures. + if m, ok := v.(map[string]interface{}); ok { + if m[rpcTokenSpecialSigKey] == rpcTokenSpecialAssetSig { + if path := m["path"]; path != nil { + return asset.NewFileAsset(cast.ToString(path)), nil + } else if text := m["text"]; text != nil { + return asset.NewStringAsset(cast.ToString(text)), nil + } else if uri := m["uri"]; uri != nil { + return asset.NewRemoteAsset(cast.ToString(uri)), nil + } + return nil, errors.New("expected asset to be one of File, String, or Remote; got none") + } else if m[rpcTokenSpecialSigKey] == rpcTokenSpecialArchiveSig { + if assets := m["assets"]; assets != nil { + as := make(map[string]interface{}) + for k, v := range assets.(map[string]interface{}) { + a, err := unmarshalOutput(v) + if err != nil { + return nil, err + } + as[k] = a + } + return asset.NewAssetArchive(as), nil + } else if path := m["path"]; path != nil { + return asset.NewFileArchive(cast.ToString(path)), nil + } else if uri := m["uri"]; uri != nil { + return asset.NewRemoteArchive(cast.ToString(uri)), nil + } + return nil, errors.New("expected asset to be one of File, String, or Remote; got none") + } + } + + // For arrays and maps, just make sure to transform them deeply. + rv := reflect.ValueOf(v) + switch rk := rv.Type().Kind(); rk { + case reflect.Array, reflect.Slice: + // If an array or a slice, create a new array by recursing into elements. + var arr []interface{} + for i := 0; i < rv.Len(); i++ { + elem := rv.Index(i) + e, err := unmarshalOutput(elem.Interface()) + if err != nil { + return nil, err + } + arr = append(arr, e) + } + return arr, nil + case reflect.Map: + // For maps, only support string-based keys, and recurse into the values. + var obj map[string]interface{} + for _, key := range rv.MapKeys() { + k, ok := key.Interface().(string) + if !ok { + return nil, errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface())) + } + value := rv.MapIndex(key) + v, err := unmarshalOutput(value) + if err != nil { + return nil, err + } + + obj[k] = v + } + return obj, nil + } + + return v, nil } diff --git a/sdk/go/pulumi/rpc_test.go b/sdk/go/pulumi/rpc_test.go new file mode 100644 index 000000000..3e3d487c4 --- /dev/null +++ b/sdk/go/pulumi/rpc_test.go @@ -0,0 +1,88 @@ +// 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 pulumi + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/pulumi/pulumi/sdk/go/pulumi/asset" +) + +// TestMarshalRoundtrip ensures that marshaling a complex structure to and from its on-the-wire gRPC format succeeds. +func TestMarshalRoundtrip(t *testing.T) { + // Create interesting inputs. + out, resolve, _ := NewOutput(nil) + resolve("outputty") + input := map[string]interface{}{ + "s": "a string", + "a": true, + "b": 42, + "cStringAsset": asset.NewStringAsset("put a lime in the coconut"), + "cFileAsset": asset.NewFileAsset("foo.txt"), + "cRemoteAsset": asset.NewRemoteAsset("https://pulumi.com/fake/asset.txt"), + "dAssetArchive": asset.NewAssetArchive(map[string]interface{}{ + "subAsset": asset.NewFileAsset("bar.txt"), + "subArchive": asset.NewFileArchive("bar.zip"), + }), + "dFileArchive": asset.NewFileArchive("foo.zip"), + "dRemoteArchive": asset.NewRemoteArchive("https://pulumi.com/fake/archive.zip"), + "e": out, + "fArray": []interface{}{0, 1.3, "x", false}, + "fMap": map[string]interface{}{ + "x": "y", + "y": 999.9, + "z": false, + }, + } + + // Marshal those inputs. + _, m, deps, err := marshalInputs(input) + if !assert.Nil(t, err) { + assert.Equal(t, 0, len(deps)) + + // Now just unmarshal and ensure the resulting map matches. + res, err := unmarshalOutputs(m) + if !assert.Nil(t, err) { + if !assert.NotNil(t, res) { + assert.Equal(t, "a string", res["s"]) + assert.Equal(t, true, res["a"]) + assert.Equal(t, 42, res["b"]) + assert.Equal(t, "put a lime in the coconut", res["cStringAsset"].(asset.Asset).Text()) + assert.Equal(t, "foo.txt", res["cFileAsset"].(asset.Asset).Path()) + assert.Equal(t, "https://pulumi.com/fake/asset.txt", res["cRemoteAsset"].(asset.Asset).URI()) + ar := res["dAssetArchive"].(asset.Archive).Assets() + assert.Equal(t, 2, len(ar)) + assert.Equal(t, "bar.txt", ar["subAsset"].(asset.Asset).Path()) + assert.Equal(t, "bar.zip", ar["subrchive"].(asset.Archive).Path()) + assert.Equal(t, "foo.zip", res["dFileArchive"].(asset.Archive).Path()) + assert.Equal(t, "https://pulumi.com/fake/archive.zip", res["dRemoteArchive"].(asset.Archive).URI()) + assert.Equal(t, "outputty", res["e"]) + aa := res["fArray"].([]interface{}) + assert.Equal(t, 4, len(aa)) + assert.Equal(t, 0, aa[0]) + assert.Equal(t, 1.3, aa[1]) + assert.Equal(t, "x", aa[2]) + assert.Equal(t, false, aa[3]) + am := res["fMap"].(map[string]interface{}) + assert.Equal(t, 3, len(am)) + assert.Equal(t, "y", am["x"]) + assert.Equal(t, 999.9, am["y"]) + assert.Equal(t, false, am["z"]) + } + } + } +} From 6e524907067e5976c8a58f03245b2240b8a35eba Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 9 Jun 2018 11:33:45 -0700 Subject: [PATCH 05/19] Fix a bunch of lint warnings --- sdk/go/pulumi/asset/asset.go | 21 ++++++++++++++++++--- sdk/go/pulumi/context.go | 2 +- sdk/go/pulumi/properties.go | 12 ++++++------ sdk/go/pulumi/rpc.go | 15 ++++++++------- sdk/go/pulumi/run.go | 8 +++++--- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/sdk/go/pulumi/asset/asset.go b/sdk/go/pulumi/asset/asset.go index e544dd2f2..f7c2ff432 100644 --- a/sdk/go/pulumi/asset/asset.go +++ b/sdk/go/pulumi/asset/asset.go @@ -37,21 +37,29 @@ type asset struct { uri string } +// NewFileAsset creates an asset backed by a file and specified by that file's path. func NewFileAsset(path string) Asset { return &asset{path: path} } +// NewStringAsset creates an asset backed by a piece of in-memory text. func NewStringAsset(text string) Asset { return &asset{text: text} } +// NewRemoteAsset creates an asset backed by a remote file and specified by that file's URL. func NewRemoteAsset(uri string) Asset { return &asset{uri: uri} } +// Path returns the asset's file path, if this is a file asset, or an empty string otherwise. func (a *asset) Path() string { return a.path } + +// Text returns the asset's textual string, if this is a string asset, or an empty string otherwise. func (a *asset) Text() string { return a.text } -func (a *asset) URI() string { return a.uri } + +// URI returns the asset's URL, if this is a remote asset, or an empty string otherwise. +func (a *asset) URI() string { return a.uri } // Archive represents a collection of Assets. type Archive interface { @@ -82,14 +90,21 @@ func NewAssetArchive(assets map[string]interface{}) Archive { return &archive{assets: assets} } +// NewFileArchive creates an archive backed by a file and specified by that file's path. func NewFileArchive(path string) Archive { return &archive{path: path} } +// NewRemoteArchive creates an archive backed by a remote file and specified by that file's URL. func NewRemoteArchive(uri string) Archive { return &archive{uri: uri} } +// Assets returns the archive's asset map, if this is a collection of archives/assets, or nil otherwise. func (a *archive) Assets() map[string]interface{} { return a.assets } -func (a *archive) Path() string { return a.path } -func (a *archive) URI() string { return a.uri } + +// Path returns the archive's file path, if this is a file archive, or an empty string otherwise. +func (a *archive) Path() string { return a.path } + +// URI returns the archive's URL, if this is a remote archive, or an empty string otherwise. +func (a *archive) URI() string { return a.uri } diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index e9274904a..3c79440d7 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -212,7 +212,7 @@ func (ctx *Context) RegisterResource( reject(err) } } else { - glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s %d", resp.Urn, resp.Id, len(outprops)) + glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s %d", t, name, resp.Urn, resp.Id, len(outprops)) resolveURN(URN(resp.Urn)) if resolveID != nil { resolveID(ID(resp.Id)) diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 92f5d8091..13e4cf551 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -151,7 +151,7 @@ func (out *Output) Asset() (asset.Asset, error) { return v.(asset.Asset), nil } -/// Bool retrives the underlying value for this output property as a bool. +// Bool retrives the underlying value for this output property as a bool. func (out *Output) Bool() (bool, error) { v, err := out.Value() if err != nil { @@ -250,7 +250,7 @@ func (out *Output) String() (string, error) { return cast.ToString(v), nil } -// Uuint retrives the underlying value for this output property as a uint. +// Uint retrives the underlying value for this output property as a uint. func (out *Output) Uint() (uint, error) { v, err := out.Value() if err != nil { @@ -259,7 +259,7 @@ func (out *Output) Uint() (uint, error) { return cast.ToUint(v), nil } -// Uuint8 retrives the underlying value for this output property as a uint8. +// Uint8 retrives the underlying value for this output property as a uint8. func (out *Output) Uint8() (uint8, error) { v, err := out.Value() if err != nil { @@ -268,7 +268,7 @@ func (out *Output) Uint8() (uint8, error) { return cast.ToUint8(v), nil } -// Uuint16 retrives the underlying value for this output property as a uint16. +// Uint16 retrives the underlying value for this output property as a uint16. func (out *Output) Uint16() (uint16, error) { v, err := out.Value() if err != nil { @@ -277,7 +277,7 @@ func (out *Output) Uint16() (uint16, error) { return cast.ToUint16(v), nil } -// Uuint32 retrives the underlying value for this output property as a uint32. +// Uint32 retrives the underlying value for this output property as a uint32. func (out *Output) Uint32() (uint32, error) { v, err := out.Value() if err != nil { @@ -286,7 +286,7 @@ func (out *Output) Uint32() (uint32, error) { return cast.ToUint32(v), nil } -// Uuint64 retrives the underlying value for this output property as a uint64. +// Uint64 retrives the underlying value for this output property as a uint64. func (out *Output) Uint64() (uint64, error) { v, err := out.Value() if err != nil { diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go index bfdacee71..4f9c65470 100644 --- a/sdk/go/pulumi/rpc.go +++ b/sdk/go/pulumi/rpc.go @@ -61,6 +61,7 @@ func marshalInputs(props map[string]interface{}) ([]string, *structpb.Struct, [] } const ( + // nolint: gas, linter thinks these are creds, but they aren't. rpcTokenSpecialSigKey = "4dabf18193072939515e22adb298388d" rpcTokenSpecialAssetSig = "c44067f5952c0a294b673a41bacd8c17" rpcTokenSpecialArchiveSig = "0def7320c3a5731c473e5ecbe6d01bc7" @@ -106,12 +107,12 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { }, nil, nil case *Output: // Await the value and return its raw value. - v, err := t.Value() + ov, err := t.Value() if err != nil { return nil, nil, err } // TODO: unknownValue - e, d, err := marshalInput(v) + e, d, err := marshalInput(ov) if err != nil { return nil, nil, err } @@ -153,12 +154,12 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface())) } value := rv.MapIndex(key) - v, d, err := marshalInput(value.Interface()) + mv, d, err := marshalInput(value.Interface()) if err != nil { return nil, nil, err } - obj[k] = v + obj[k] = mv deps = append(deps, d...) } return obj, deps, nil @@ -243,19 +244,19 @@ func unmarshalOutput(v interface{}) (interface{}, error) { return arr, nil case reflect.Map: // For maps, only support string-based keys, and recurse into the values. - var obj map[string]interface{} + obj := make(map[string]interface{}) for _, key := range rv.MapKeys() { k, ok := key.Interface().(string) if !ok { return nil, errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface())) } value := rv.MapIndex(key) - v, err := unmarshalOutput(value) + mv, err := unmarshalOutput(value) if err != nil { return nil, err } - obj[k] = v + obj[k] = mv } return obj, nil } diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go index af2a0c932..976c57224 100644 --- a/sdk/go/pulumi/run.go +++ b/sdk/go/pulumi/run.go @@ -22,6 +22,8 @@ import ( "github.com/hashicorp/go-multierror" "golang.org/x/net/context" + + "github.com/pulumi/pulumi/pkg/util/contract" ) // Run executes the body of a Pulumi program, granting it access to a deployment context that it may use @@ -34,7 +36,7 @@ func Run(body RunFunc) { } } -// Run executes the body of a Pulumi program, granting it access to a deployment context that it may use +// RunErr executes the body of a Pulumi program, granting it access to a deployment context that it may use // to register resources and orchestrate deployment activities. This connects back to the Pulumi engine using gRPC. func RunErr(body RunFunc) error { // Parse the info out of environment variables. This is a lame contract with the caller, but helps to keep @@ -47,7 +49,7 @@ func RunErr(body RunFunc) error { if err != nil { return err } - defer ctx.Close() + defer contract.IgnoreClose(ctx) // Create a root stack resource that we'll parent everything to. reg, err := ctx.RegisterResource( @@ -75,7 +77,7 @@ func RunErr(body RunFunc) error { } // Propagate the error from the body, if any. - return err + return result } // RunFunc executes the body of a Pulumi program. It may register resources using the deployment context From 25b1a0c9c3cb0c818727aa82772097e483db5240 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 9 Jun 2018 16:16:07 -0700 Subject: [PATCH 06/19] Wire up RegisterResource to unmarshalOutputs --- sdk/go/pulumi/context.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index 3c79440d7..e5e3a105c 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -23,8 +23,6 @@ import ( "golang.org/x/net/context" "google.golang.org/grpc" - "github.com/pulumi/pulumi/pkg/resource" - "github.com/pulumi/pulumi/pkg/resource/plugin" pulumirpc "github.com/pulumi/pulumi/sdk/proto/go" ) @@ -197,10 +195,7 @@ func (ctx *Context) RegisterResource( }) var outprops map[string]interface{} if err == nil { - var uprops resource.PropertyMap - if uprops, err = plugin.UnmarshalProperties(resp.Object, plugin.MarshalOptions{}); err == nil { - outprops = uprops.Mappable() - } + outprops, err = unmarshalOutputs(resp.Object) } if err != nil { glog.V(9).Infof("RegisterResource(%s, %s): error: %v", t, name, err) From 5a71ab9d1242f1d4ec141ff7176fb510a66ad331 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 9 Jun 2018 16:16:35 -0700 Subject: [PATCH 07/19] Add Makefile machinery for Go provider --- Makefile | 2 +- scripts/make_release.sh | 1 + sdk/go/Makefile | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 sdk/go/Makefile diff --git a/Makefile b/Makefile index fa0c95d9d..6af294ac0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PROJECT_NAME := Pulumi SDK -SUB_PROJECTS := sdk/nodejs sdk/python +SUB_PROJECTS := sdk/nodejs sdk/python sdk/go include build/common.mk PROJECT := github.com/pulumi/pulumi diff --git a/scripts/make_release.sh b/scripts/make_release.sh index 3c5890b54..6d311380b 100755 --- a/scripts/make_release.sh +++ b/scripts/make_release.sh @@ -47,6 +47,7 @@ copy_package() { run_go_build "${ROOT}" run_go_build "${ROOT}/sdk/nodejs/cmd/pulumi-language-nodejs" run_go_build "${ROOT}/sdk/python/cmd/pulumi-language-python" +run_go_build "${ROOT}/sdk/go/pulumi-language-go" # Copy over the language and dynamic resource providers. cp ${ROOT}/sdk/nodejs/dist/pulumi-resource-pulumi-nodejs ${PUBDIR}/bin/ diff --git a/sdk/go/Makefile b/sdk/go/Makefile new file mode 100644 index 000000000..6f05edaa6 --- /dev/null +++ b/sdk/go/Makefile @@ -0,0 +1,24 @@ +PROJECT_NAME := Pulumi Go SDK +LANGHOST_PKG := github.com/pulumi/pulumi/sdk/go/pulumi-language-go +VERSION := $(shell ../../scripts/get-version) +PROJECT_PKGS := $(shell go list ./pulumi/... ./pulumi-language-go/... | grep -v /vendor/) + +GOMETALINTERBIN := gometalinter +GOMETALINTER := ${GOMETALINTERBIN} --config=../../Gometalinter.json + +TESTPARALLELISM := 10 + +include ../../build/common.mk + +build:: + go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGHOST_PKG} + +install:: + GOBIN=$(PULUMI_BIN) go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGHOST_PKG} + +lint:: + $(GOMETALINTER) ./pulumi/... | sort ; exit $$(($${PIPESTATUS[1]}-1)) + $(GOMETALINTER) ./pulumi-language-go/... | sort ; exit $$(($${PIPESTATUS[1]}-1)) + +test_fast:: + go test -cover -parallel ${TESTPARALLELISM} ${PROJECT_PKGS} From 20af051cafd4b6b3d469aa3cd65bf5c05d4b3b1c Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 9 Jun 2018 16:59:53 -0700 Subject: [PATCH 08/19] Implement unknown outputs This commit implements unknown outputs in the same style as our Node.js language provider. That is to say, during previews, it's possible that certain outputs will not have known values. In those cases, we want to flow sufficient information through the resolution of values, so that we may skip applies. We also return this fact from the direct accessors. --- sdk/go/pulumi/context.go | 12 +- sdk/go/pulumi/properties.go | 321 +++++++++++++++++-------------- sdk/go/pulumi/properties_test.go | 90 ++++++--- sdk/go/pulumi/rpc.go | 23 ++- sdk/go/pulumi/rpc_test.go | 2 +- 5 files changed, 264 insertions(+), 184 deletions(-) diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index e5e3a105c..1eb0f9a93 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -162,14 +162,14 @@ func (ctx *Context) RegisterResource( urn, resolveURN, rejectURN := NewOutput(nil) var id *Output - var resolveID func(interface{}) + var resolveID func(interface{}, bool) var rejectID func(error) if custom { id, resolveID, rejectID = NewOutput(nil) } state := make(map[string]*Output) - resolveState := make(map[string]func(interface{})) + resolveState := make(map[string]func(interface{}, bool)) rejectState := make(map[string]func(error)) for _, key := range keys { state[key], resolveState[key], rejectState[key] = NewOutput(nil) @@ -208,16 +208,18 @@ func (ctx *Context) RegisterResource( } } else { glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s %d", t, name, resp.Urn, resp.Id, len(outprops)) - resolveURN(URN(resp.Urn)) + resolveURN(URN(resp.Urn), true) if resolveID != nil { - resolveID(ID(resp.Id)) + resolveID(ID(resp.Id), true) } for _, key := range keys { out, err := unmarshalOutput(outprops[key]) if err != nil { rejectState[key](err) } else { - resolveState[key](out) + // During previews, it's possible that nils will be returned due to unknown values. + known := !ctx.DryRun() || out != nil + resolveState[key](out, known) } } } diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 13e4cf551..688e4ee28 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -34,12 +34,13 @@ type Output struct { type valueOrError struct { value interface{} // a value, if the output resolved to a value. err error // an error, if the producer yielded an error instead of a value. + known bool // true if this value is known, versus just being a placeholder during previews. } // NewOutput returns an output value that can be used to rendezvous with the production of a value or error. The // function returns the output itself, plus two functions: one for resolving a value, and another for rejecting with an // error; exactly one function must be called. This acts like a promise. -func NewOutput(deps []Resource) (*Output, func(interface{}), func(error)) { +func NewOutput(deps []Resource) (*Output, func(interface{}, bool), func(error)) { out := &Output{ sync: make(chan *valueOrError, 1), deps: deps, @@ -49,19 +50,19 @@ func NewOutput(deps []Resource) (*Output, func(interface{}), func(error)) { // resolve will resolve the output. It is not exported, because we want to control the capabilities tightly, such // that anybody who happens to have an Output is not allowed to resolve it; only those who created it can. -func (out *Output) resolve(v interface{}) { +func (out *Output) resolve(v interface{}, known bool) { // If v is another output, chain this rather than resolving to an output directly. - if other, isOut := v.(*Output); isOut { + if other, isOut := v.(*Output); known && isOut { go func() { - real, err := other.Value() + real, otherKnown, err := other.Value() if err != nil { out.reject(err) } else { - out.resolve(real) + out.resolve(real, otherKnown) } }() } else { - out.sync <- &valueOrError{value: v} + out.sync <- &valueOrError{value: v, known: known} } } @@ -78,27 +79,33 @@ func (out *Output) Apply(applier func(v interface{}) (interface{}, error)) *Outp result, resolve, reject := NewOutput(out.Deps()) go func() { for { - v, err := out.Value() + v, known, err := out.Value() if err != nil { reject(err) break } else { - // If we have a value, run the applier to transform it. - u, err := applier(v) - if err != nil { - reject(err) - break - } else { - // Now that we've transformed the value, it's possible we have another output. If so, pluck it - // out and go around to await it until we hit a real value. Note that we are not capturing the - // resources of this inner output, intentionally, as the output returned should be related to - // this output already. - if newout, ok := v.(*Output); ok { - out = newout - } else { - resolve(u) + if known { + // If we have a known value, run the applier to transform it. + u, err := applier(v) + if err != nil { + reject(err) break + } else { + // Now that we've transformed the value, it's possible we have another output. If so, pluck it + // out and go around to await it until we hit a real value. Note that we are not capturing the + // resources of this inner output, intentionally, as the output returned should be related to + // this output already. + if newout, ok := v.(*Output); ok { + out = newout + } else { + resolve(u, true) + break + } } + } else { + // If the value isn't known, skip the apply function. + resolve(nil, false) + break } } } @@ -107,12 +114,10 @@ func (out *Output) Apply(applier func(v interface{}) (interface{}, error)) *Outp } // Deps returns the dependencies for this output property. -func (out *Output) Deps() []Resource { - return out.deps -} +func (out *Output) Deps() []Resource { return out.deps } // Value retrieves the underlying value for this output property. -func (out *Output) Value() (interface{}, error) { +func (out *Output) Value() (interface{}, bool, error) { // If neither error nor value are available, first await the channel. Only one Goroutine will make it through this // and is responsible for closing the channel, to signal to other awaiters that it's safe to read the values. if out.voe == nil { @@ -121,184 +126,184 @@ func (out *Output) Value() (interface{}, error) { close(out.sync) } } - return out.voe.value, out.voe.err + return out.voe.value, out.voe.known, out.voe.err } // Archive retrives the underlying value for this output property as an archive. -func (out *Output) Archive() (asset.Archive, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Archive() (asset.Archive, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return v.(asset.Archive), nil + return v.(asset.Archive), true, nil } // Array retrives the underlying value for this output property as an array. -func (out *Output) Array() ([]interface{}, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Array() ([]interface{}, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return cast.ToSlice(v), nil + return cast.ToSlice(v), true, nil } // Asset retrives the underlying value for this output property as an asset. -func (out *Output) Asset() (asset.Asset, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Asset() (asset.Asset, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return v.(asset.Asset), nil + return v.(asset.Asset), true, nil } // Bool retrives the underlying value for this output property as a bool. -func (out *Output) Bool() (bool, error) { - v, err := out.Value() - if err != nil { - return false, err +func (out *Output) Bool() (bool, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return false, known, err } - return cast.ToBool(v), nil + return cast.ToBool(v), true, nil } // Map retrives the underlying value for this output property as a map. -func (out *Output) Map() (map[string]interface{}, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Map() (map[string]interface{}, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return cast.ToStringMap(v), nil + return cast.ToStringMap(v), true, nil } // Float32 retrives the underlying value for this output property as a float32. -func (out *Output) Float32() (float32, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Float32() (float32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToFloat32(v), nil + return cast.ToFloat32(v), true, nil } // Float64 retrives the underlying value for this output property as a float64. -func (out *Output) Float64() (float64, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Float64() (float64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToFloat64(v), nil + return cast.ToFloat64(v), true, nil } // ID retrives the underlying value for this output property as an ID. -func (out *Output) ID() (ID, error) { - v, err := out.Value() - if err != nil { - return "", err +func (out *Output) ID() (ID, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return "", known, err } - return ID(cast.ToString(v)), nil + return ID(cast.ToString(v)), true, nil } // Int retrives the underlying value for this output property as a int. -func (out *Output) Int() (int, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int() (int, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt(v), nil + return cast.ToInt(v), true, nil } // Int8 retrives the underlying value for this output property as a int8. -func (out *Output) Int8() (int8, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int8() (int8, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt8(v), nil + return cast.ToInt8(v), true, nil } // Int16 retrives the underlying value for this output property as a int16. -func (out *Output) Int16() (int16, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int16() (int16, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt16(v), nil + return cast.ToInt16(v), true, nil } // Int32 retrives the underlying value for this output property as a int32. -func (out *Output) Int32() (int32, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int32() (int32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt32(v), nil + return cast.ToInt32(v), true, nil } // Int64 retrives the underlying value for this output property as a int64. -func (out *Output) Int64() (int64, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int64() (int64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt64(v), nil + return cast.ToInt64(v), true, nil } // String retrives the underlying value for this output property as a string. -func (out *Output) String() (string, error) { - v, err := out.Value() - if err != nil { - return "", err +func (out *Output) String() (string, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return "", known, err } - return cast.ToString(v), nil + return cast.ToString(v), true, nil } // Uint retrives the underlying value for this output property as a uint. -func (out *Output) Uint() (uint, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint() (uint, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint(v), nil + return cast.ToUint(v), true, nil } // Uint8 retrives the underlying value for this output property as a uint8. -func (out *Output) Uint8() (uint8, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint8() (uint8, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint8(v), nil + return cast.ToUint8(v), true, nil } // Uint16 retrives the underlying value for this output property as a uint16. -func (out *Output) Uint16() (uint16, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint16() (uint16, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint16(v), nil + return cast.ToUint16(v), true, nil } // Uint32 retrives the underlying value for this output property as a uint32. -func (out *Output) Uint32() (uint32, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint32() (uint32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint32(v), nil + return cast.ToUint32(v), true, nil } // Uint64 retrives the underlying value for this output property as a uint64. -func (out *Output) Uint64() (uint64, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint64() (uint64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint64(v), nil + return cast.ToUint64(v), true, nil } // URN retrives the underlying value for this output property as a URN. func (out *Output) URN() (URN, error) { - v, err := out.Value() - if err != nil { + v, known, err := out.Value() + if err != nil || !known { return "", err } return URN(cast.ToString(v)), nil @@ -311,7 +316,9 @@ type Outputs map[string]*Output type ArchiveOutput Output // Value returns the underlying archive value. -func (out *ArchiveOutput) Value() (asset.Archive, error) { return (*Output)(out).Archive() } +func (out *ArchiveOutput) Value() (asset.Archive, bool, error) { + return (*Output)(out).Archive() +} // Apply applies a transformation to the archive value when it is available. func (out *ArchiveOutput) Apply(applier func(asset.Archive) (interface{}, error)) *Output { @@ -324,7 +331,9 @@ func (out *ArchiveOutput) Apply(applier func(asset.Archive) (interface{}, error) type ArrayOutput Output // Value returns the underlying array value. -func (out *ArrayOutput) Value() ([]interface{}, error) { return (*Output)(out).Array() } +func (out *ArrayOutput) Value() ([]interface{}, bool, error) { + return (*Output)(out).Array() +} // Apply applies a transformation to the array value when it is available. func (out *ArrayOutput) Apply(applier func([]interface{}) (interface{}, error)) *Output { @@ -337,7 +346,9 @@ func (out *ArrayOutput) Apply(applier func([]interface{}) (interface{}, error)) type AssetOutput Output // Value returns the underlying asset value. -func (out *AssetOutput) Value() (asset.Asset, error) { return (*Output)(out).Asset() } +func (out *AssetOutput) Value() (asset.Asset, bool, error) { + return (*Output)(out).Asset() +} // Apply applies a transformation to the asset value when it is available. func (out *AssetOutput) Apply(applier func(asset.Asset) (interface{}, error)) *Output { @@ -350,7 +361,9 @@ func (out *AssetOutput) Apply(applier func(asset.Asset) (interface{}, error)) *O type BoolOutput Output // Value returns the underlying bool value. -func (out *BoolOutput) Value() (bool, error) { return (*Output)(out).Bool() } +func (out *BoolOutput) Value() (bool, bool, error) { + return (*Output)(out).Bool() +} // Apply applies a transformation to the bool value when it is available. func (out *BoolOutput) Apply(applier func(bool) (interface{}, error)) *Output { @@ -363,7 +376,9 @@ func (out *BoolOutput) Apply(applier func(bool) (interface{}, error)) *Output { type Float32Output Output // Value returns the underlying number value. -func (out *Float32Output) Value() (float32, error) { return (*Output)(out).Float32() } +func (out *Float32Output) Value() (float32, bool, error) { + return (*Output)(out).Float32() +} // Apply applies a transformation to the float32 value when it is available. func (out *Float32Output) Apply(applier func(float32) (interface{}, error)) *Output { @@ -376,7 +391,9 @@ func (out *Float32Output) Apply(applier func(float32) (interface{}, error)) *Out type Float64Output Output // Value returns the underlying number value. -func (out *Float64Output) Value() (float64, error) { return (*Output)(out).Float64() } +func (out *Float64Output) Value() (float64, bool, error) { + return (*Output)(out).Float64() +} // Apply applies a transformation to the float64 value when it is available. func (out *Float64Output) Apply(applier func(float64) (interface{}, error)) *Output { @@ -389,7 +406,9 @@ func (out *Float64Output) Apply(applier func(float64) (interface{}, error)) *Out type IntOutput Output // Value returns the underlying number value. -func (out *IntOutput) Value() (int, error) { return (*Output)(out).Int() } +func (out *IntOutput) Value() (int, bool, error) { + return (*Output)(out).Int() +} // Apply applies a transformation to the int value when it is available. func (out *IntOutput) Apply(applier func(int) (interface{}, error)) *Output { @@ -402,7 +421,9 @@ func (out *IntOutput) Apply(applier func(int) (interface{}, error)) *Output { type Int8Output Output // Value returns the underlying number value. -func (out *Int8Output) Value() (int8, error) { return (*Output)(out).Int8() } +func (out *Int8Output) Value() (int8, bool, error) { + return (*Output)(out).Int8() +} // Apply applies a transformation to the int8 value when it is available. func (out *Int8Output) Apply(applier func(int8) (interface{}, error)) *Output { @@ -415,7 +436,9 @@ func (out *Int8Output) Apply(applier func(int8) (interface{}, error)) *Output { type Int16Output Output // Value returns the underlying number value. -func (out *Int16Output) Value() (int16, error) { return (*Output)(out).Int16() } +func (out *Int16Output) Value() (int16, bool, error) { + return (*Output)(out).Int16() +} // Apply applies a transformation to the int16 value when it is available. func (out *Int16Output) Apply(applier func(int16) (interface{}, error)) *Output { @@ -428,7 +451,9 @@ func (out *Int16Output) Apply(applier func(int16) (interface{}, error)) *Output type Int32Output Output // Value returns the underlying number value. -func (out *Int32Output) Value() (int32, error) { return (*Output)(out).Int32() } +func (out *Int32Output) Value() (int32, bool, error) { + return (*Output)(out).Int32() +} // Apply applies a transformation to the int32 value when it is available. func (out *Int32Output) Apply(applier func(int32) (interface{}, error)) *Output { @@ -441,7 +466,7 @@ func (out *Int32Output) Apply(applier func(int32) (interface{}, error)) *Output type Int64Output Output // Value returns the underlying number value. -func (out *Int64Output) Value() (int64, error) { return (*Output)(out).Int64() } +func (out *Int64Output) Value() (int64, bool, error) { return (*Output)(out).Int64() } // Apply applies a transformation to the int64 value when it is available. func (out *Int64Output) Apply(applier func(int64) (interface{}, error)) *Output { @@ -454,7 +479,9 @@ func (out *Int64Output) Apply(applier func(int64) (interface{}, error)) *Output type MapOutput Output // Value returns the underlying number value. -func (out *MapOutput) Value() (map[string]interface{}, error) { return (*Output)(out).Map() } +func (out *MapOutput) Value() (map[string]interface{}, bool, error) { + return (*Output)(out).Map() +} // Apply applies a transformation to the number value when it is available. func (out *MapOutput) Apply(applier func(map[string]interface{}) (interface{}, error)) *Output { @@ -467,7 +494,9 @@ func (out *MapOutput) Apply(applier func(map[string]interface{}) (interface{}, e type StringOutput Output // Value returns the underlying number value. -func (out *StringOutput) Value() (string, error) { return (*Output)(out).String() } +func (out *StringOutput) Value() (string, bool, error) { + return (*Output)(out).String() +} // Apply applies a transformation to the number value when it is available. func (out *StringOutput) Apply(applier func(string) (interface{}, error)) *Output { @@ -480,7 +509,9 @@ func (out *StringOutput) Apply(applier func(string) (interface{}, error)) *Outpu type UintOutput Output // Value returns the underlying number value. -func (out *UintOutput) Value() (uint, error) { return (*Output)(out).Uint() } +func (out *UintOutput) Value() (uint, bool, error) { + return (*Output)(out).Uint() +} // Apply applies a transformation to the uint value when it is available. func (out *UintOutput) Apply(applier func(uint) (interface{}, error)) *Output { @@ -493,7 +524,9 @@ func (out *UintOutput) Apply(applier func(uint) (interface{}, error)) *Output { type Uint8Output Output // Value returns the underlying number value. -func (out *Uint8Output) Value() (uint8, error) { return (*Output)(out).Uint8() } +func (out *Uint8Output) Value() (uint8, bool, error) { + return (*Output)(out).Uint8() +} // Apply applies a transformation to the uint8 value when it is available. func (out *Uint8Output) Apply(applier func(uint8) (interface{}, error)) *Output { @@ -506,7 +539,9 @@ func (out *Uint8Output) Apply(applier func(uint8) (interface{}, error)) *Output type Uint16Output Output // Value returns the underlying number value. -func (out *Uint16Output) Value() (uint16, error) { return (*Output)(out).Uint16() } +func (out *Uint16Output) Value() (uint16, bool, error) { + return (*Output)(out).Uint16() +} // Apply applies a transformation to the uint16 value when it is available. func (out *Uint16Output) Apply(applier func(uint16) (interface{}, error)) *Output { @@ -519,7 +554,9 @@ func (out *Uint16Output) Apply(applier func(uint16) (interface{}, error)) *Outpu type Uint32Output Output // Value returns the underlying number value. -func (out *Uint32Output) Value() (uint32, error) { return (*Output)(out).Uint32() } +func (out *Uint32Output) Value() (uint32, bool, error) { + return (*Output)(out).Uint32() +} // Apply applies a transformation to the uint32 value when it is available. func (out *Uint32Output) Apply(applier func(uint32) (interface{}, error)) *Output { @@ -532,7 +569,9 @@ func (out *Uint32Output) Apply(applier func(uint32) (interface{}, error)) *Outpu type Uint64Output Output // Value returns the underlying number value. -func (out *Uint64Output) Value() (uint64, error) { return (*Output)(out).Uint64() } +func (out *Uint64Output) Value() (uint64, bool, error) { + return (*Output)(out).Uint64() +} // Apply applies a transformation to the uint64 value when it is available. func (out *Uint64Output) Apply(applier func(uint64) (interface{}, error)) *Output { diff --git a/sdk/go/pulumi/properties_test.go b/sdk/go/pulumi/properties_test.go index f0222c25e..a2af8b46f 100644 --- a/sdk/go/pulumi/properties_test.go +++ b/sdk/go/pulumi/properties_test.go @@ -26,10 +26,11 @@ func TestBasicOutputs(t *testing.T) { { out, resolve, _ := NewOutput(nil) go func() { - resolve(42) + resolve(42, true) }() - v, err := out.Value() + v, known, err := out.Value() assert.Nil(t, err) + assert.True(t, known) assert.NotNil(t, v) assert.Equal(t, 42, v.(int)) } @@ -38,7 +39,7 @@ func TestBasicOutputs(t *testing.T) { go func() { reject(errors.New("boom")) }() - v, err := out.Value() + v, _, err := out.Value() assert.NotNil(t, err) assert.Nil(t, v) } @@ -47,11 +48,12 @@ func TestBasicOutputs(t *testing.T) { func TestArrayOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve([]interface{}{nil, 0, "x"}) + resolve([]interface{}{nil, 0, "x"}, true) }() { - v, err := out.Array() + v, known, err := out.Array() assert.Nil(t, err) + assert.True(t, known) assert.NotNil(t, v) if assert.Equal(t, 3, len(v)) { assert.Equal(t, nil, v[0]) @@ -61,7 +63,7 @@ func TestArrayOutputs(t *testing.T) { } { arr := (*ArrayOutput)(out) - v, err := arr.Value() + v, _, err := arr.Value() assert.Nil(t, err) assert.NotNil(t, v) if assert.Equal(t, 3, len(v)) { @@ -75,17 +77,19 @@ func TestArrayOutputs(t *testing.T) { func TestBoolOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve(true) + resolve(true, true) }() { - v, err := out.Bool() + v, known, err := out.Bool() assert.Nil(t, err) + assert.True(t, known) assert.True(t, v) } { b := (*BoolOutput)(out) - v, err := b.Value() + v, known, err := b.Value() assert.Nil(t, err) + assert.True(t, known) assert.True(t, v) } } @@ -97,11 +101,12 @@ func TestMapOutputs(t *testing.T) { "x": 1, "y": false, "z": "abc", - }) + }, true) }() { - v, err := out.Map() + v, known, err := out.Map() assert.Nil(t, err) + assert.True(t, known) assert.NotNil(t, v) assert.Equal(t, 1, v["x"]) assert.Equal(t, false, v["y"]) @@ -109,8 +114,9 @@ func TestMapOutputs(t *testing.T) { } { b := (*MapOutput)(out) - v, err := b.Value() + v, known, err := b.Value() assert.Nil(t, err) + assert.True(t, known) assert.NotNil(t, v) assert.Equal(t, 1, v["x"]) assert.Equal(t, false, v["y"]) @@ -121,17 +127,19 @@ func TestMapOutputs(t *testing.T) { func TestNumberOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve(42.345) + resolve(42.345, true) }() { - v, err := out.Float64() + v, known, err := out.Float64() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, 42.345, v) } { b := (*Float64Output)(out) - v, err := b.Value() + v, known, err := b.Value() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, 42.345, v) } } @@ -139,17 +147,19 @@ func TestNumberOutputs(t *testing.T) { func TestStringOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve("a stringy output") + resolve("a stringy output", true) }() { - v, err := out.String() + v, known, err := out.String() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, "a stringy output", v) } { b := (*StringOutput)(out) - v, err := b.Value() + v, known, err := b.Value() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, "a stringy output", v) } } @@ -160,11 +170,12 @@ func TestResolveOutputToOutput(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { other, resolveOther, _ := NewOutput(nil) - resolve(other) - go func() { resolveOther(99) }() + resolve(other, true) + go func() { resolveOther(99, true) }() }() - v, err := out.Value() + v, known, err := out.Value() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, v, 99) } // Similarly, test that resolving an output to a rejected output yields an error. @@ -172,10 +183,10 @@ func TestResolveOutputToOutput(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { other, _, rejectOther := NewOutput(nil) - resolve(other) + resolve(other, true) go func() { rejectOther(errors.New("boom")) }() }() - v, err := out.Value() + v, _, err := out.Value() assert.NotNil(t, err) assert.Nil(t, v) } @@ -185,18 +196,34 @@ func TestOutputApply(t *testing.T) { // Test that resolved outputs lead to applies being run. { out, resolve, _ := NewOutput(nil) - go func() { resolve(42) }() + go func() { resolve(42, true) }() var ranApp bool b := (*IntOutput)(out) app := b.Apply(func(v int) (interface{}, error) { ranApp = true return v + 1, nil }) - v, err := app.Value() + v, known, err := app.Value() assert.True(t, ranApp) assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, v, 43) } + // Test that resolved, but known outputs, skip the running of applies. + { + out, resolve, _ := NewOutput(nil) + go func() { resolve(42, false) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + ranApp = true + return v + 1, nil + }) + _, known, err := app.Value() + assert.False(t, ranApp) + assert.Nil(t, err) + assert.False(t, known) + } // Test that rejected outputs do not run the apply, and instead flow the error. { out, _, reject := NewOutput(nil) @@ -207,7 +234,7 @@ func TestOutputApply(t *testing.T) { ranApp = true return v + 1, nil }) - v, err := app.Value() + v, _, err := app.Value() assert.False(t, ranApp) assert.NotNil(t, err) assert.Nil(t, v) @@ -215,24 +242,25 @@ func TestOutputApply(t *testing.T) { // Test that an an apply that returns an output returns the resolution of that output, not the output itself. { out, resolve, _ := NewOutput(nil) - go func() { resolve(42) }() + go func() { resolve(42, true) }() var ranApp bool b := (*IntOutput)(out) app := b.Apply(func(v int) (interface{}, error) { other, resolveOther, _ := NewOutput(nil) - go func() { resolveOther(v + 1) }() + go func() { resolveOther(v+1, true) }() ranApp = true return other, nil }) - v, err := app.Value() + v, known, err := app.Value() assert.True(t, ranApp) assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, v, 43) } // Test that an an apply that reject an output returns the rejection of that output, not the output itself. { out, resolve, _ := NewOutput(nil) - go func() { resolve(42) }() + go func() { resolve(42, true) }() var ranApp bool b := (*IntOutput)(out) app := b.Apply(func(v int) (interface{}, error) { @@ -241,7 +269,7 @@ func TestOutputApply(t *testing.T) { ranApp = true return other, nil }) - v, err := app.Value() + v, _, err := app.Value() assert.True(t, ranApp) assert.NotNil(t, err) assert.Nil(t, v) diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go index 4f9c65470..498bacaac 100644 --- a/sdk/go/pulumi/rpc.go +++ b/sdk/go/pulumi/rpc.go @@ -107,16 +107,22 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { }, nil, nil case *Output: // Await the value and return its raw value. - ov, err := t.Value() + ov, known, err := t.Value() if err != nil { return nil, nil, err } - // TODO: unknownValue - e, d, err := marshalInput(ov) - if err != nil { - return nil, nil, err + + if known { + // If the value is known, marshal it. + e, d, merr := marshalInput(ov) + if merr != nil { + return nil, nil, merr + } + return e, append(t.Deps(), d...), nil + } else { + // Otherwise, simply return the unknown value sentinel. + return rpcTokenUnknownValue, t.Deps(), nil } - return e, append(t.Deps(), d...), err case CustomResource: // Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a e, d, err := marshalInput(t.ID()) @@ -196,6 +202,11 @@ func unmarshalOutputs(outs *structpb.Struct) (map[string]interface{}, error) { // unmarshalOutput unmarshals a single output variable into its runtime representation. For the most part, this just // returns the raw value. In a small number of cases, we need to change a type. func unmarshalOutput(v interface{}) (interface{}, error) { + // Check for nils and unknowns. + if v == nil || v == rpcTokenUnknownValue { + return nil, nil + } + // In the case of assets and archives, turn these into real asset and archive structures. if m, ok := v.(map[string]interface{}); ok { if m[rpcTokenSpecialSigKey] == rpcTokenSpecialAssetSig { diff --git a/sdk/go/pulumi/rpc_test.go b/sdk/go/pulumi/rpc_test.go index 3e3d487c4..11e83769d 100644 --- a/sdk/go/pulumi/rpc_test.go +++ b/sdk/go/pulumi/rpc_test.go @@ -26,7 +26,7 @@ import ( func TestMarshalRoundtrip(t *testing.T) { // Create interesting inputs. out, resolve, _ := NewOutput(nil) - resolve("outputty") + resolve("outputty", true) input := map[string]interface{}{ "s": "a string", "a": true, From cf1cb2d61f73dbaa05f3a5e2c863f639b9393d4b Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 9 Jun 2018 17:23:12 -0700 Subject: [PATCH 09/19] Get lint clean --- sdk/go/Makefile | 4 ++-- sdk/go/pulumi/rpc.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/go/Makefile b/sdk/go/Makefile index 6f05edaa6..f9f4b53d6 100644 --- a/sdk/go/Makefile +++ b/sdk/go/Makefile @@ -17,8 +17,8 @@ install:: GOBIN=$(PULUMI_BIN) go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGHOST_PKG} lint:: - $(GOMETALINTER) ./pulumi/... | sort ; exit $$(($${PIPESTATUS[1]}-1)) - $(GOMETALINTER) ./pulumi-language-go/... | sort ; exit $$(($${PIPESTATUS[1]}-1)) + $(GOMETALINTER) ./pulumi/... | sort + $(GOMETALINTER) ./pulumi-language-go/... | sort test_fast:: go test -cover -parallel ${TESTPARALLELISM} ${PROJECT_PKGS} diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go index 498bacaac..1f78bda00 100644 --- a/sdk/go/pulumi/rpc.go +++ b/sdk/go/pulumi/rpc.go @@ -112,17 +112,17 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { return nil, nil, err } + // If the value is known, marshal it. if known { - // If the value is known, marshal it. e, d, merr := marshalInput(ov) if merr != nil { return nil, nil, merr } return e, append(t.Deps(), d...), nil - } else { - // Otherwise, simply return the unknown value sentinel. - return rpcTokenUnknownValue, t.Deps(), nil } + + // Otherwise, simply return the unknown value sentinel. + return rpcTokenUnknownValue, t.Deps(), nil case CustomResource: // Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a e, d, err := marshalInput(t.ID()) From da0667ef458757d80bf27eeef5d689ea22224b4e Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 07:24:38 -0700 Subject: [PATCH 10/19] Add config package This change adds a config package. This is syntactic sugar atop the underlying config functionality in the pulumi.Context, but mirrors what we do in our other Node.js and Python SDKs more closely. This includes three families of functions: - config.Get*: returns the value or its default if missing. - config.Require*: returns the value or panics if missing. - config.Try*: returns the value or an error if missing. In all cases, there are simple Get/Require/Try functions, that just deal in terms of strings, in addition to type specific functions, GetT/RequireT/TryT, for the most common Ts that you might need. --- sdk/go/pulumi/config/config.go | 232 ++++++++++++++++++++++++++++ sdk/go/pulumi/config/config_test.go | 79 ++++++++++ sdk/go/pulumi/config/get.go | 131 ++++++++++++++++ sdk/go/pulumi/config/require.go | 109 +++++++++++++ sdk/go/pulumi/config/try.go | 149 ++++++++++++++++++ sdk/go/pulumi/context.go | 61 ++++---- sdk/go/pulumi/run.go | 12 ++ 7 files changed, 747 insertions(+), 26 deletions(-) create mode 100644 sdk/go/pulumi/config/config_test.go create mode 100644 sdk/go/pulumi/config/get.go create mode 100644 sdk/go/pulumi/config/require.go create mode 100644 sdk/go/pulumi/config/try.go diff --git a/sdk/go/pulumi/config/config.go b/sdk/go/pulumi/config/config.go index 28d179d46..2ad374a38 100644 --- a/sdk/go/pulumi/config/config.go +++ b/sdk/go/pulumi/config/config.go @@ -13,3 +13,235 @@ // limitations under the License. package pulumi + +import ( + "github.com/pulumi/pulumi/sdk/go/pulumi" +) + +// Config is a struct that permits access to config as a "bag" with a package name. This avoids needing to access +// config with the fully qualified name all of the time (e.g., a bag whose namespace is "p" automatically translates +// attempted reads of keys "k" into "p:k"). This is optional but can save on some boilerplate when accessing config. +type Config struct { + ctx *pulumi.Context + namespace string +} + +// New creates a new config bag with the given context and namespace. +func New(ctx *pulumi.Context, namespace string) *Config { + return &Config{ctx: ctx, namespace: namespace} +} + +// fullKey turns a simple configuration key into a fully resolved one, by prepending the bag's name. +func (c *Config) fullKey(key string) string { + return c.namespace + ":" + key +} + +// Get loads an optional configuration value by its key, or returns "" if it doesn't exist. +func (c *Config) Get(key string) string { + return Get(c.ctx, c.fullKey(key)) +} + +// GetBool loads an optional bool configuration value by its key, or returns false if it doesn't exist. +func (c *Config) GetBool(key string) bool { + return GetBool(c.ctx, c.fullKey(key)) +} + +// GetFloat32 loads an optional float32 configuration value by its key, or returns 0.0 if it doesn't exist. +func (c *Config) GetFloat32(key string) float32 { + return GetFloat32(c.ctx, c.fullKey(key)) +} + +// GetFloat64 loads an optional float64 configuration value by its key, or returns 0.0 if it doesn't exist. +func (c *Config) GetFloat64(key string) float64 { + return GetFloat64(c.ctx, c.fullKey(key)) +} + +// GetInt loads an optional int configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetInt(key string) int { + return GetInt(c.ctx, c.fullKey(key)) +} + +// GetInt8 loads an optional int8 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetInt8(key string) int8 { + return GetInt8(c.ctx, c.fullKey(key)) +} + +// GetInt16 loads an optional int16 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetInt16(key string) int16 { + return GetInt16(c.ctx, c.fullKey(key)) +} + +// GetInt32 loads an optional int32 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetInt32(key string) int32 { + return GetInt32(c.ctx, c.fullKey(key)) +} + +// GetInt64 loads an optional int64 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetInt64(key string) int64 { + return GetInt64(c.ctx, c.fullKey(key)) +} + +// GetUint loads an optional uint configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetUint(key string) uint { + return GetUint(c.ctx, c.fullKey(key)) +} + +// GetUint8 loads an optional uint8 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetUint8(key string) uint8 { + return GetUint8(c.ctx, c.fullKey(key)) +} + +// GetUint16 loads an optional uint16 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetUint16(key string) uint16 { + return GetUint16(c.ctx, c.fullKey(key)) +} + +// GetUint32 loads an optional uint32 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetUint32(key string) uint32 { + return GetUint32(c.ctx, c.fullKey(key)) +} + +// GetUint64 loads an optional uint64 configuration value by its key, or returns 0 if it doesn't exist. +func (c *Config) GetUint64(key string) uint64 { + return GetUint64(c.ctx, c.fullKey(key)) +} + +// Require loads a configuration value by its key, or panics if it doesn't exist. +func (c *Config) Require(key string) string { + return Require(c.ctx, c.fullKey(key)) +} + +// RequireBool loads a bool configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireBool(key string) bool { + return RequireBool(c.ctx, c.fullKey(key)) +} + +// RequireFloat32 loads a float32 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireFloat32(key string) float32 { + return RequireFloat32(c.ctx, c.fullKey(key)) +} + +// RequireFloat64 loads a float64 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireFloat64(key string) float64 { + return RequireFloat64(c.ctx, c.fullKey(key)) +} + +// RequireInt loads a int configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireInt(key string) int { + return RequireInt(c.ctx, c.fullKey(key)) +} + +// RequireInt8 loads a int8 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireInt8(key string) int8 { + return RequireInt8(c.ctx, c.fullKey(key)) +} + +// RequireInt16 loads a int16 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireInt16(key string) int16 { + return RequireInt16(c.ctx, c.fullKey(key)) +} + +// RequireInt32 loads a int32 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireInt32(key string) int32 { + return RequireInt32(c.ctx, c.fullKey(key)) +} + +// RequireInt64 loads a int64 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireInt64(key string) int64 { + return RequireInt64(c.ctx, c.fullKey(key)) +} + +// RequireUint loads a uint configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireUint(key string) uint { + return RequireUint(c.ctx, c.fullKey(key)) +} + +// RequireUint8 loads a uint8 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireUint8(key string) uint8 { + return RequireUint8(c.ctx, c.fullKey(key)) +} + +// RequireUint16 loads a uint16 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireUint16(key string) uint16 { + return RequireUint16(c.ctx, c.fullKey(key)) +} + +// RequireUint32 loads a uint32 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireUint32(key string) uint32 { + return RequireUint32(c.ctx, c.fullKey(key)) +} + +// RequireUint64 loads a uint64 configuration value by its key, or panics if it doesn't exist. +func (c *Config) RequireUint64(key string) uint64 { + return RequireUint64(c.ctx, c.fullKey(key)) +} + +// Try loads a configuration value by its key, returning a non-nil error if it doesn't exist. +func (c *Config) Try(key string) (string, error) { + return Try(c.ctx, c.fullKey(key)) +} + +// TryBool loads an optional bool configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryBool(key string) (bool, error) { + return TryBool(c.ctx, c.fullKey(key)) +} + +// TryFloat32 loads an optional float32 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryFloat32(key string) (float32, error) { + return TryFloat32(c.ctx, c.fullKey(key)) +} + +// TryFloat64 loads an optional float64 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryFloat64(key string) (float64, error) { + return TryFloat64(c.ctx, c.fullKey(key)) +} + +// TryInt loads an optional int configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryInt(key string) (int, error) { + return TryInt(c.ctx, c.fullKey(key)) +} + +// TryInt8 loads an optional int8 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryInt8(key string) (int8, error) { + return TryInt8(c.ctx, c.fullKey(key)) +} + +// TryInt16 loads an optional int16 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryInt16(key string) (int16, error) { + return TryInt16(c.ctx, c.fullKey(key)) +} + +// TryInt32 loads an optional int32 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryInt32(key string) (int32, error) { + return TryInt32(c.ctx, c.fullKey(key)) +} + +// TryInt64 loads an optional int64 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryInt64(key string) (int64, error) { + return TryInt64(c.ctx, c.fullKey(key)) +} + +// TryUint loads an optional uint configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryUint(key string) (uint, error) { + return TryUint(c.ctx, c.fullKey(key)) +} + +// TryUint8 loads an optional uint8 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryUint8(key string) (uint8, error) { + return TryUint8(c.ctx, c.fullKey(key)) +} + +// TryUint16 loads an optional uint16 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryUint16(key string) (uint16, error) { + return TryUint16(c.ctx, c.fullKey(key)) +} + +// TryUint32 loads an optional uint32 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryUint32(key string) (uint32, error) { + return TryUint32(c.ctx, c.fullKey(key)) +} + +// TryUint64 loads an optional uint64 configuration value by its key, or returns an error if it doesn't exist. +func (c *Config) TryUint64(key string) (uint64, error) { + return TryUint64(c.ctx, c.fullKey(key)) +} diff --git a/sdk/go/pulumi/config/config_test.go b/sdk/go/pulumi/config/config_test.go new file mode 100644 index 000000000..786543104 --- /dev/null +++ b/sdk/go/pulumi/config/config_test.go @@ -0,0 +1,79 @@ +// 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 pulumi + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/pulumi/pulumi/sdk/go/pulumi" +) + +// TestConfig tests the basic config wrapper. +func TestConfig(t *testing.T) { + ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{ + Config: map[string]string{ + "testpkg:sss": "a string value", + "testpkg:bbb": "true", + "testpkg:intint": "42", + "testpkg:fpfpfp": "99.963", + }, + }) + assert.Nil(t, err) + + cfg := New(ctx, "testpkg") + + // Test basic keys. + assert.Equal(t, "testpkg:sss", cfg.fullKey("sss")) + + // Test Get, which returns a default value for missing entries rather than failing. + assert.Equal(t, "a string value", cfg.Get("sss")) + assert.Equal(t, true, cfg.GetBool("bbb")) + assert.Equal(t, 42, cfg.GetInt("intint")) + assert.Equal(t, 99.963, cfg.GetFloat64("fpfpfp")) + assert.Equal(t, "", cfg.Get("missing")) + + // Test Require, which panics for missing entries. + assert.Equal(t, "a string value", cfg.Require("sss")) + assert.Equal(t, true, cfg.RequireBool("bbb")) + assert.Equal(t, 42, cfg.RequireInt("intint")) + assert.Equal(t, 99.963, cfg.RequireFloat64("fpfpfp")) + func() { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected missing key for Require to panic") + } + }() + _ = cfg.Require("missing") + }() + + // Test Try, which returns an error for missing entries. + k1, err := cfg.Try("sss") + assert.Nil(t, err) + assert.Equal(t, "a string value", k1) + k2, err := cfg.TryBool("bbb") + assert.Nil(t, err) + assert.Equal(t, true, k2) + k3, err := cfg.TryInt("intint") + assert.Nil(t, err) + assert.Equal(t, 42, k3) + k4, err := cfg.TryFloat64("fpfpfp") + assert.Nil(t, err) + assert.Equal(t, 99.963, k4) + _, err = cfg.Try("missing") + assert.NotNil(t, err) +} diff --git a/sdk/go/pulumi/config/get.go b/sdk/go/pulumi/config/get.go new file mode 100644 index 000000000..e5ab5c592 --- /dev/null +++ b/sdk/go/pulumi/config/get.go @@ -0,0 +1,131 @@ +// 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 pulumi + +import ( + "github.com/spf13/cast" + + "github.com/pulumi/pulumi/sdk/go/pulumi" +) + +// Get loads an optional configuration value by its key, or returns "" if it doesn't exist. +func Get(ctx *pulumi.Context, key string) string { + v, _ := ctx.GetConfig(key) + return v +} + +// GetBool loads an optional configuration value by its key, as a bool, or returns false if it doesn't exist. +func GetBool(ctx *pulumi.Context, key string) bool { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToBool(v) + } + return false +} + +// GetFloat32 loads an optional configuration value by its key, as a float32, or returns 0.0 if it doesn't exist. +func GetFloat32(ctx *pulumi.Context, key string) float32 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToFloat32(v) + } + return 0 +} + +// GetFloat64 loads an optional configuration value by its key, as a float64, or returns 0.0 if it doesn't exist. +func GetFloat64(ctx *pulumi.Context, key string) float64 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToFloat64(v) + } + return 0 +} + +// GetInt loads an optional configuration value by its key, as a int, or returns 0 if it doesn't exist. +func GetInt(ctx *pulumi.Context, key string) int { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToInt(v) + } + return 0 +} + +// GetInt8 loads an optional configuration value by its key, as a int8, or returns 0 if it doesn't exist. +func GetInt8(ctx *pulumi.Context, key string) int8 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToInt8(v) + } + return 0 +} + +// GetInt16 loads an optional configuration value by its key, as a int16, or returns 0 if it doesn't exist. +func GetInt16(ctx *pulumi.Context, key string) int16 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToInt16(v) + } + return 0 +} + +// GetInt32 loads an optional configuration value by its key, as a int32, or returns 0 if it doesn't exist. +func GetInt32(ctx *pulumi.Context, key string) int32 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToInt32(v) + } + return 0 +} + +// GetInt64 loads an optional configuration value by its key, as a int64, or returns 0 if it doesn't exist. +func GetInt64(ctx *pulumi.Context, key string) int64 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToInt64(v) + } + return 0 +} + +// GetUint loads an optional configuration value by its key, as a uint, or returns 0 if it doesn't exist. +func GetUint(ctx *pulumi.Context, key string) uint { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToUint(v) + } + return 0 +} + +// GetUint8 loads an optional configuration value by its key, as a uint8, or returns 0 if it doesn't exist. +func GetUint8(ctx *pulumi.Context, key string) uint8 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToUint8(v) + } + return 0 +} + +// GetUint16 loads an optional configuration value by its key, as a uint16, or returns 0 if it doesn't exist. +func GetUint16(ctx *pulumi.Context, key string) uint16 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToUint16(v) + } + return 0 +} + +// GetUint32 loads an optional configuration value by its key, as a uint32, or returns 0 if it doesn't exist. +func GetUint32(ctx *pulumi.Context, key string) uint32 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToUint32(v) + } + return 0 +} + +// GetUint64 loads an optional configuration value by its key, as a uint64, or returns 0 if it doesn't exist. +func GetUint64(ctx *pulumi.Context, key string) uint64 { + if v, ok := ctx.GetConfig(key); ok { + return cast.ToUint64(v) + } + return 0 +} diff --git a/sdk/go/pulumi/config/require.go b/sdk/go/pulumi/config/require.go new file mode 100644 index 000000000..6f6bd98ca --- /dev/null +++ b/sdk/go/pulumi/config/require.go @@ -0,0 +1,109 @@ +// 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 pulumi + +import ( + "github.com/spf13/cast" + + "github.com/pulumi/pulumi/pkg/util/contract" + "github.com/pulumi/pulumi/sdk/go/pulumi" +) + +// Require loads a configuration value by its key, or panics if it doesn't exist. +func Require(ctx *pulumi.Context, key string) string { + v, ok := ctx.GetConfig(key) + if !ok { + contract.Failf("missing required configuration variable '%s'; run `pulumi config` to set", key) + } + return v +} + +// Require loads an optional configuration value by its key, as a bool, or panics if it doesn't exist. +func RequireBool(ctx *pulumi.Context, key string) bool { + v := Require(ctx, key) + return cast.ToBool(v) +} + +// Require loads an optional configuration value by its key, as a float32, or panics if it doesn't exist. +func RequireFloat32(ctx *pulumi.Context, key string) float32 { + v := Require(ctx, key) + return cast.ToFloat32(v) +} + +// Require loads an optional configuration value by its key, as a float64, or panics if it doesn't exist. +func RequireFloat64(ctx *pulumi.Context, key string) float64 { + v := Require(ctx, key) + return cast.ToFloat64(v) +} + +// Require loads an optional configuration value by its key, as a int, or panics if it doesn't exist. +func RequireInt(ctx *pulumi.Context, key string) int { + v := Require(ctx, key) + return cast.ToInt(v) +} + +// Require loads an optional configuration value by its key, as a int8, or panics if it doesn't exist. +func RequireInt8(ctx *pulumi.Context, key string) int8 { + v := Require(ctx, key) + return cast.ToInt8(v) +} + +// Require loads an optional configuration value by its key, as a int16, or panics if it doesn't exist. +func RequireInt16(ctx *pulumi.Context, key string) int16 { + v := Require(ctx, key) + return cast.ToInt16(v) +} + +// Require loads an optional configuration value by its key, as a int32, or panics if it doesn't exist. +func RequireInt32(ctx *pulumi.Context, key string) int32 { + v := Require(ctx, key) + return cast.ToInt32(v) +} + +// Require loads an optional configuration value by its key, as a int64, or panics if it doesn't exist. +func RequireInt64(ctx *pulumi.Context, key string) int64 { + v := Require(ctx, key) + return cast.ToInt64(v) +} + +// Require loads an optional configuration value by its key, as a uint, or panics if it doesn't exist. +func RequireUint(ctx *pulumi.Context, key string) uint { + v := Require(ctx, key) + return cast.ToUint(v) +} + +// Require loads an optional configuration value by its key, as a uint8, or panics if it doesn't exist. +func RequireUint8(ctx *pulumi.Context, key string) uint8 { + v := Require(ctx, key) + return cast.ToUint8(v) +} + +// Require loads an optional configuration value by its key, as a uint16, or panics if it doesn't exist. +func RequireUint16(ctx *pulumi.Context, key string) uint16 { + v := Require(ctx, key) + return cast.ToUint16(v) +} + +// Require loads an optional configuration value by its key, as a uint32, or panics if it doesn't exist. +func RequireUint32(ctx *pulumi.Context, key string) uint32 { + v := Require(ctx, key) + return cast.ToUint32(v) +} + +// Require loads an optional configuration value by its key, as a uint64, or panics if it doesn't exist. +func RequireUint64(ctx *pulumi.Context, key string) uint64 { + v := Require(ctx, key) + return cast.ToUint64(v) +} diff --git a/sdk/go/pulumi/config/try.go b/sdk/go/pulumi/config/try.go new file mode 100644 index 000000000..cd999b2ec --- /dev/null +++ b/sdk/go/pulumi/config/try.go @@ -0,0 +1,149 @@ +// 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 pulumi + +import ( + "github.com/pkg/errors" + "github.com/spf13/cast" + + "github.com/pulumi/pulumi/sdk/go/pulumi" +) + +// Try loads a configuration value by its key, returning a non-nil error if it doesn't exist. +func Try(ctx *pulumi.Context, key string) (string, error) { + v, ok := ctx.GetConfig(key) + if !ok { + return "", + errors.Errorf("missing required configuration variable '%s'; run `pulumi config` to set", key) + } + return v, nil +} + +// Try loads an optional configuration value by its key, as a bool, or returns an error if it doesn't exist. +func TryBool(ctx *pulumi.Context, key string) (bool, error) { + v, err := Try(ctx, key) + if err != nil { + return false, err + } + return cast.ToBool(v), nil +} + +// Try loads an optional configuration value by its key, as a float32, or returns an error if it doesn't exist. +func TryFloat32(ctx *pulumi.Context, key string) (float32, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToFloat32(v), nil +} + +// Try loads an optional configuration value by its key, as a float64, or returns an error if it doesn't exist. +func TryFloat64(ctx *pulumi.Context, key string) (float64, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToFloat64(v), nil +} + +// Try loads an optional configuration value by its key, as a int, or returns an error if it doesn't exist. +func TryInt(ctx *pulumi.Context, key string) (int, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToInt(v), nil +} + +// Try loads an optional configuration value by its key, as a int8, or returns an error if it doesn't exist. +func TryInt8(ctx *pulumi.Context, key string) (int8, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToInt8(v), nil +} + +// Try loads an optional configuration value by its key, as a int16, or returns an error if it doesn't exist. +func TryInt16(ctx *pulumi.Context, key string) (int16, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToInt16(v), nil +} + +// Try loads an optional configuration value by its key, as a int32, or returns an error if it doesn't exist. +func TryInt32(ctx *pulumi.Context, key string) (int32, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToInt32(v), nil +} + +// Try loads an optional configuration value by its key, as a int64, or returns an error if it doesn't exist. +func TryInt64(ctx *pulumi.Context, key string) (int64, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToInt64(v), nil +} + +// Try loads an optional configuration value by its key, as a uint, or returns an error if it doesn't exist. +func TryUint(ctx *pulumi.Context, key string) (uint, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToUint(v), nil +} + +// Try loads an optional configuration value by its key, as a uint8, or returns an error if it doesn't exist. +func TryUint8(ctx *pulumi.Context, key string) (uint8, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToUint8(v), nil +} + +// Try loads an optional configuration value by its key, as a uint16, or returns an error if it doesn't exist. +func TryUint16(ctx *pulumi.Context, key string) (uint16, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToUint16(v), nil +} + +// Try loads an optional configuration value by its key, as a uint32, or returns an error if it doesn't exist. +func TryUint32(ctx *pulumi.Context, key string) (uint32, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToUint32(v), nil +} + +// Try loads an optional configuration value by its key, as a uint64, or returns an error if it doesn't exist. +func TryUint64(ctx *pulumi.Context, key string) (uint64, error) { + v, err := Try(ctx, key) + if err != nil { + return 0, err + } + return cast.ToUint64(v), nil +} diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index 1eb0f9a93..a3efab00c 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -43,28 +43,27 @@ type Context struct { // NewContext creates a fresh run context out of the given metadata. func NewContext(ctx context.Context, info RunInfo) (*Context, error) { - // Validate some properties. - if info.Project == "" { - return nil, errors.New("missing project name") - } - if info.Stack == "" { - return nil, errors.New("missing stack name") - } - if info.MonitorAddr == "" { - return nil, errors.New("missing resource monitor RPC address") - } - if info.EngineAddr == "" { - return nil, errors.New("missing engine RPC address") + // Connect to the gRPC endpoints if we have addresses for them. + var monitorConn *grpc.ClientConn + var monitor pulumirpc.ResourceMonitorClient + if addr := info.MonitorAddr; addr != "" { + conn, err := grpc.Dial(info.MonitorAddr, grpc.WithInsecure()) + if err != nil { + return nil, errors.Wrap(err, "connecting to resource monitor over RPC") + } + monitorConn = conn + monitor = pulumirpc.NewResourceMonitorClient(monitorConn) } - monitorConn, err := grpc.Dial(info.MonitorAddr, grpc.WithInsecure()) - if err != nil { - return nil, errors.Wrap(err, "connecting to resource monitor over RPC") - } - - engineConn, err := grpc.Dial(info.EngineAddr, grpc.WithInsecure()) - if err != nil { - return nil, errors.Wrap(err, "connecting to engine over RPC") + var engineConn *grpc.ClientConn + var engine pulumirpc.EngineClient + if addr := info.EngineAddr; addr != "" { + conn, err := grpc.Dial(info.EngineAddr, grpc.WithInsecure()) + if err != nil { + return nil, errors.Wrap(err, "connecting to engine over RPC") + } + engineConn = conn + engine = pulumirpc.NewEngineClient(engineConn) } mutex := &sync.Mutex{} @@ -73,9 +72,9 @@ func NewContext(ctx context.Context, info RunInfo) (*Context, error) { info: info, exports: make(map[string]interface{}), monitorConn: monitorConn, - monitor: pulumirpc.NewResourceMonitorClient(monitorConn), + monitor: monitor, engineConn: engineConn, - engine: pulumirpc.NewEngineClient(engineConn), + engine: engine, rpcs: 0, rpcsLock: mutex, rpcsDone: sync.NewCond(mutex), @@ -84,11 +83,15 @@ func NewContext(ctx context.Context, info RunInfo) (*Context, error) { // Close implements io.Closer and relinquishes any outstanding resources held by the context. func (ctx *Context) Close() error { - if err := ctx.engineConn.Close(); err != nil { - return err + if ctx.engineConn != nil { + if err := ctx.engineConn.Close(); err != nil { + return err + } } - if err := ctx.monitorConn.Close(); err != nil { - return err + if ctx.monitorConn != nil { + if err := ctx.monitorConn.Close(); err != nil { + return err + } } return nil } @@ -105,6 +108,12 @@ func (ctx *Context) Parallel() int { return ctx.info.Parallel } // DryRun is true when evaluating a program for purposes of planning, instead of performing a true deployment. func (ctx *Context) DryRun() bool { return ctx.info.DryRun } +// GetConfig returns the config value, as a string, and a bool indicating whether it exists or not. +func (ctx *Context) GetConfig(key string) (string, bool) { + v, ok := ctx.info.Config[key] + return v, ok +} + // Invoke will invoke a provider's function, identified by its token tok. func (ctx *Context) Invoke(tok string, args map[string]interface{}) (Outputs, error) { // TODO(joe): implement this. diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go index 976c57224..8e5ca950b 100644 --- a/sdk/go/pulumi/run.go +++ b/sdk/go/pulumi/run.go @@ -21,6 +21,7 @@ import ( "strconv" "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" "golang.org/x/net/context" "github.com/pulumi/pulumi/pkg/util/contract" @@ -44,6 +45,17 @@ func RunErr(body RunFunc) error { // TODO(joe): this is a fine default, but consider `...RunOpt`s to control how we get the various addresses, etc. info := getEnvInfo() + // Validate some properties. + if info.Project == "" { + return errors.New("missing project name") + } else if info.Stack == "" { + return errors.New("missing stack name") + } else if info.MonitorAddr == "" { + return errors.New("missing resource monitor RPC address") + } else if info.EngineAddr == "" { + return errors.New("missing engine RPC address") + } + // Create a fresh context. ctx, err := NewContext(context.TODO(), info) if err != nil { From 150ed846361907ae17384c3a62278bbf9741aea6 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 08:05:49 -0700 Subject: [PATCH 11/19] Implement context invoke function --- sdk/go/pulumi/context.go | 82 ++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index a3efab00c..24457e56f 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -19,6 +19,7 @@ import ( "sync" "github.com/golang/glog" + "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "golang.org/x/net/context" "google.golang.org/grpc" @@ -114,10 +115,51 @@ func (ctx *Context) GetConfig(key string) (string, bool) { return v, ok } -// Invoke will invoke a provider's function, identified by its token tok. -func (ctx *Context) Invoke(tok string, args map[string]interface{}) (Outputs, error) { - // TODO(joe): implement this. - return nil, errors.New("Invoke not yet implemented") +// Invoke will invoke a provider's function, identified by its token tok. This function call is synchronous. +func (ctx *Context) Invoke(tok string, args map[string]interface{}) (map[string]interface{}, error) { + if tok == "" { + return nil, errors.New("invoke token must not be empty") + } + + // Serialize arguments, first by awaiting them, and then marshaling them to the requisite gRPC values. + // TODO[pulumi/pulumi#1483]: feels like we should be propagating dependencies to the outputs, instead of ignoring. + _, rpcArgs, _, err := marshalInputs(args) + if err != nil { + return nil, errors.Wrap(err, "marshaling arguments") + } + + // Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown. + if err = ctx.beginRPC(); err != nil { + return nil, err + } + defer ctx.endRPC() + + // Now, invoke the RPC to the provider synchronously. + glog.V(9).Infof("Invoke(%s, #args=%d): RPC call being made synchronously", tok, len(args)) + resp, err := ctx.monitor.Invoke(ctx.ctx, &pulumirpc.InvokeRequest{ + Tok: tok, + Args: rpcArgs, + }) + if err != nil { + glog.V(9).Infof("Invoke(%s, ...): error: %v", tok, err) + return nil, err + } + + // If there were any failures from the provider, return them. + if len(resp.Failures) > 0 { + glog.V(9).Infof("Invoke(%s, ...): success: w/ %d failures", tok, len(resp.Failures)) + var ferr error + for _, failure := range resp.Failures { + ferr = multierror.Append(ferr, + errors.Errorf("%s invoke failed: %s (%s)", tok, failure.Reason, failure.Property)) + } + return nil, ferr + } + + // Otherwsie, simply unmarshal the output properties and return the result. + outs, err := unmarshalOutputs(resp.Return) + glog.V(9).Infof("Invoke(%s, ...): success: w/ %d outs (err=%v)", tok, len(outs), err) + return outs, err } // ReadResource reads an existing custom resource's state from the resource monitor. Note that resources read in this @@ -207,6 +249,7 @@ func (ctx *Context) RegisterResource( outprops, err = unmarshalOutputs(resp.Object) } if err != nil { + // If there was an error, we must reject everything: URN, ID, and state properties. glog.V(9).Infof("RegisterResource(%s, %s): error: %v", t, name, err) rejectURN(err) if rejectID != nil { @@ -217,18 +260,33 @@ func (ctx *Context) RegisterResource( } } else { glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s %d", t, name, resp.Urn, resp.Id, len(outprops)) + + // Resolve the URN and ID. resolveURN(URN(resp.Urn), true) if resolveID != nil { resolveID(ID(resp.Id), true) } - for _, key := range keys { - out, err := unmarshalOutput(outprops[key]) - if err != nil { - rejectState[key](err) - } else { - // During previews, it's possible that nils will be returned due to unknown values. - known := !ctx.DryRun() || out != nil - resolveState[key](out, known) + + // During previews, it's possible that nils will be returned due to unknown values. This function + // determines the known-ed-ness of a given value below. + isKnown := func(v interface{}) bool { + return !ctx.DryRun() || v != nil + } + + // Now resolve all output properties. + seen := make(map[string]bool) + for key, v := range outprops { + if resolve, has := resolveState[key]; has { + resolve(v, isKnown(v)) + seen[key] = true + } + } + + // If we didn't get back any inputs as outputs, resolve them to the inputs. + for key, resolve := range resolveState { + if !seen[key] { + v := props[key] + resolve(v, isKnown(v)) } } } From b0556c441691593b8c86e94bb582c9f2106f8214 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 08:08:02 -0700 Subject: [PATCH 12/19] Fix lint warnings on documentation --- sdk/go/pulumi/config/require.go | 26 +++++++++++++------------- sdk/go/pulumi/config/try.go | 26 +++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/sdk/go/pulumi/config/require.go b/sdk/go/pulumi/config/require.go index 6f6bd98ca..a69350f76 100644 --- a/sdk/go/pulumi/config/require.go +++ b/sdk/go/pulumi/config/require.go @@ -30,79 +30,79 @@ func Require(ctx *pulumi.Context, key string) string { return v } -// Require loads an optional configuration value by its key, as a bool, or panics if it doesn't exist. +// RequireBool loads an optional configuration value by its key, as a bool, or panics if it doesn't exist. func RequireBool(ctx *pulumi.Context, key string) bool { v := Require(ctx, key) return cast.ToBool(v) } -// Require loads an optional configuration value by its key, as a float32, or panics if it doesn't exist. +// RequireFloat32 loads an optional configuration value by its key, as a float32, or panics if it doesn't exist. func RequireFloat32(ctx *pulumi.Context, key string) float32 { v := Require(ctx, key) return cast.ToFloat32(v) } -// Require loads an optional configuration value by its key, as a float64, or panics if it doesn't exist. +// RequireFloat64 loads an optional configuration value by its key, as a float64, or panics if it doesn't exist. func RequireFloat64(ctx *pulumi.Context, key string) float64 { v := Require(ctx, key) return cast.ToFloat64(v) } -// Require loads an optional configuration value by its key, as a int, or panics if it doesn't exist. +// RequireInt loads an optional configuration value by its key, as a int, or panics if it doesn't exist. func RequireInt(ctx *pulumi.Context, key string) int { v := Require(ctx, key) return cast.ToInt(v) } -// Require loads an optional configuration value by its key, as a int8, or panics if it doesn't exist. +// RequireInt8 loads an optional configuration value by its key, as a int8, or panics if it doesn't exist. func RequireInt8(ctx *pulumi.Context, key string) int8 { v := Require(ctx, key) return cast.ToInt8(v) } -// Require loads an optional configuration value by its key, as a int16, or panics if it doesn't exist. +// RequireInt16 loads an optional configuration value by its key, as a int16, or panics if it doesn't exist. func RequireInt16(ctx *pulumi.Context, key string) int16 { v := Require(ctx, key) return cast.ToInt16(v) } -// Require loads an optional configuration value by its key, as a int32, or panics if it doesn't exist. +// RequireInt32 loads an optional configuration value by its key, as a int32, or panics if it doesn't exist. func RequireInt32(ctx *pulumi.Context, key string) int32 { v := Require(ctx, key) return cast.ToInt32(v) } -// Require loads an optional configuration value by its key, as a int64, or panics if it doesn't exist. +// RequireInt64 loads an optional configuration value by its key, as a int64, or panics if it doesn't exist. func RequireInt64(ctx *pulumi.Context, key string) int64 { v := Require(ctx, key) return cast.ToInt64(v) } -// Require loads an optional configuration value by its key, as a uint, or panics if it doesn't exist. +// RequireUint loads an optional configuration value by its key, as a uint, or panics if it doesn't exist. func RequireUint(ctx *pulumi.Context, key string) uint { v := Require(ctx, key) return cast.ToUint(v) } -// Require loads an optional configuration value by its key, as a uint8, or panics if it doesn't exist. +// RequireUint8 loads an optional configuration value by its key, as a uint8, or panics if it doesn't exist. func RequireUint8(ctx *pulumi.Context, key string) uint8 { v := Require(ctx, key) return cast.ToUint8(v) } -// Require loads an optional configuration value by its key, as a uint16, or panics if it doesn't exist. +// RequireUint16 loads an optional configuration value by its key, as a uint16, or panics if it doesn't exist. func RequireUint16(ctx *pulumi.Context, key string) uint16 { v := Require(ctx, key) return cast.ToUint16(v) } -// Require loads an optional configuration value by its key, as a uint32, or panics if it doesn't exist. +// RequireUint32 loads an optional configuration value by its key, as a uint32, or panics if it doesn't exist. func RequireUint32(ctx *pulumi.Context, key string) uint32 { v := Require(ctx, key) return cast.ToUint32(v) } -// Require loads an optional configuration value by its key, as a uint64, or panics if it doesn't exist. +// RequireUint64 loads an optional configuration value by its key, as a uint64, or panics if it doesn't exist. func RequireUint64(ctx *pulumi.Context, key string) uint64 { v := Require(ctx, key) return cast.ToUint64(v) diff --git a/sdk/go/pulumi/config/try.go b/sdk/go/pulumi/config/try.go index cd999b2ec..0b5ed1e4a 100644 --- a/sdk/go/pulumi/config/try.go +++ b/sdk/go/pulumi/config/try.go @@ -31,7 +31,7 @@ func Try(ctx *pulumi.Context, key string) (string, error) { return v, nil } -// Try loads an optional configuration value by its key, as a bool, or returns an error if it doesn't exist. +// TryBool loads an optional configuration value by its key, as a bool, or returns an error if it doesn't exist. func TryBool(ctx *pulumi.Context, key string) (bool, error) { v, err := Try(ctx, key) if err != nil { @@ -40,7 +40,7 @@ func TryBool(ctx *pulumi.Context, key string) (bool, error) { return cast.ToBool(v), nil } -// Try loads an optional configuration value by its key, as a float32, or returns an error if it doesn't exist. +// TryFloat32 loads an optional configuration value by its key, as a float32, or returns an error if it doesn't exist. func TryFloat32(ctx *pulumi.Context, key string) (float32, error) { v, err := Try(ctx, key) if err != nil { @@ -49,7 +49,7 @@ func TryFloat32(ctx *pulumi.Context, key string) (float32, error) { return cast.ToFloat32(v), nil } -// Try loads an optional configuration value by its key, as a float64, or returns an error if it doesn't exist. +// TryFloat64 loads an optional configuration value by its key, as a float64, or returns an error if it doesn't exist. func TryFloat64(ctx *pulumi.Context, key string) (float64, error) { v, err := Try(ctx, key) if err != nil { @@ -58,7 +58,7 @@ func TryFloat64(ctx *pulumi.Context, key string) (float64, error) { return cast.ToFloat64(v), nil } -// Try loads an optional configuration value by its key, as a int, or returns an error if it doesn't exist. +// TryInt loads an optional configuration value by its key, as a int, or returns an error if it doesn't exist. func TryInt(ctx *pulumi.Context, key string) (int, error) { v, err := Try(ctx, key) if err != nil { @@ -67,7 +67,7 @@ func TryInt(ctx *pulumi.Context, key string) (int, error) { return cast.ToInt(v), nil } -// Try loads an optional configuration value by its key, as a int8, or returns an error if it doesn't exist. +// TryInt8 loads an optional configuration value by its key, as a int8, or returns an error if it doesn't exist. func TryInt8(ctx *pulumi.Context, key string) (int8, error) { v, err := Try(ctx, key) if err != nil { @@ -76,7 +76,7 @@ func TryInt8(ctx *pulumi.Context, key string) (int8, error) { return cast.ToInt8(v), nil } -// Try loads an optional configuration value by its key, as a int16, or returns an error if it doesn't exist. +// TryInt16 loads an optional configuration value by its key, as a int16, or returns an error if it doesn't exist. func TryInt16(ctx *pulumi.Context, key string) (int16, error) { v, err := Try(ctx, key) if err != nil { @@ -85,7 +85,7 @@ func TryInt16(ctx *pulumi.Context, key string) (int16, error) { return cast.ToInt16(v), nil } -// Try loads an optional configuration value by its key, as a int32, or returns an error if it doesn't exist. +// TryInt32 loads an optional configuration value by its key, as a int32, or returns an error if it doesn't exist. func TryInt32(ctx *pulumi.Context, key string) (int32, error) { v, err := Try(ctx, key) if err != nil { @@ -94,7 +94,7 @@ func TryInt32(ctx *pulumi.Context, key string) (int32, error) { return cast.ToInt32(v), nil } -// Try loads an optional configuration value by its key, as a int64, or returns an error if it doesn't exist. +// TryInt64 loads an optional configuration value by its key, as a int64, or returns an error if it doesn't exist. func TryInt64(ctx *pulumi.Context, key string) (int64, error) { v, err := Try(ctx, key) if err != nil { @@ -103,7 +103,7 @@ func TryInt64(ctx *pulumi.Context, key string) (int64, error) { return cast.ToInt64(v), nil } -// Try loads an optional configuration value by its key, as a uint, or returns an error if it doesn't exist. +// TryUint loads an optional configuration value by its key, as a uint, or returns an error if it doesn't exist. func TryUint(ctx *pulumi.Context, key string) (uint, error) { v, err := Try(ctx, key) if err != nil { @@ -112,7 +112,7 @@ func TryUint(ctx *pulumi.Context, key string) (uint, error) { return cast.ToUint(v), nil } -// Try loads an optional configuration value by its key, as a uint8, or returns an error if it doesn't exist. +// TryUint8 loads an optional configuration value by its key, as a uint8, or returns an error if it doesn't exist. func TryUint8(ctx *pulumi.Context, key string) (uint8, error) { v, err := Try(ctx, key) if err != nil { @@ -121,7 +121,7 @@ func TryUint8(ctx *pulumi.Context, key string) (uint8, error) { return cast.ToUint8(v), nil } -// Try loads an optional configuration value by its key, as a uint16, or returns an error if it doesn't exist. +// TryUint16 loads an optional configuration value by its key, as a uint16, or returns an error if it doesn't exist. func TryUint16(ctx *pulumi.Context, key string) (uint16, error) { v, err := Try(ctx, key) if err != nil { @@ -130,7 +130,7 @@ func TryUint16(ctx *pulumi.Context, key string) (uint16, error) { return cast.ToUint16(v), nil } -// Try loads an optional configuration value by its key, as a uint32, or returns an error if it doesn't exist. +// TryUint32 loads an optional configuration value by its key, as a uint32, or returns an error if it doesn't exist. func TryUint32(ctx *pulumi.Context, key string) (uint32, error) { v, err := Try(ctx, key) if err != nil { @@ -139,7 +139,7 @@ func TryUint32(ctx *pulumi.Context, key string) (uint32, error) { return cast.ToUint32(v), nil } -// Try loads an optional configuration value by its key, as a uint64, or returns an error if it doesn't exist. +// TryUint64 loads an optional configuration value by its key, as a uint64, or returns an error if it doesn't exist. func TryUint64(ctx *pulumi.Context, key string) (uint64, error) { v, err := Try(ctx, key) if err != nil { From b10b7d9b8a6983883e3f963b37d3776717a8c0e4 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 08:51:30 -0700 Subject: [PATCH 13/19] Implement ReadResource RPC functionality --- sdk/go/pulumi/context.go | 279 ++++++++++++++++++++++++------------ sdk/go/pulumi/properties.go | 30 ++++ sdk/go/pulumi/run.go | 2 +- 3 files changed, 222 insertions(+), 89 deletions(-) diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index 24457e56f..103a2dcf4 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -19,6 +19,7 @@ import ( "sync" "github.com/golang/glog" + structpb "github.com/golang/protobuf/ptypes/struct" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "golang.org/x/net/context" @@ -165,7 +166,7 @@ func (ctx *Context) Invoke(tok string, args map[string]interface{}) (map[string] // ReadResource reads an existing custom resource's state from the resource monitor. Note that resources read in this // way will not be part of the resulting stack's state, as they are presumed to belong to another. func (ctx *Context) ReadResource( - t, name string, id ID, state map[string]interface{}, opts ...ResourceOpt) (*ResourceState, error) { + t, name string, id ID, props map[string]interface{}, opts ...ResourceOpt) (*ResourceState, error) { if t == "" { return nil, errors.New("resource type argument cannot be empty") } else if name == "" { @@ -174,7 +175,48 @@ func (ctx *Context) ReadResource( return nil, errors.New("resource ID is required for lookup and cannot be empty") } - return nil, errors.New("ReadResource not yet implemented") + // Prepare the inputs for an impending operation. + op, err := ctx.newResourceOperation(true, props, opts...) + if err != nil { + return nil, err + } + + // Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown. + if err = ctx.beginRPC(); err != nil { + return nil, err + } + + // Kick off the resource read operation. This will happen asynchronously and resolve the above properties. + go func() { + glog.V(9).Infof("ReadResource(%s, %s): Goroutine spawned, RPC call being made", t, name) + resp, err := ctx.monitor.ReadResource(ctx.ctx, &pulumirpc.ReadResourceRequest{ + Type: t, + Name: name, + Parent: op.parent, + Properties: op.rpcProps, + }) + if err != nil { + glog.V(9).Infof("RegisterResource(%s, %s): error: %v", t, name, err) + } else { + glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s ...", t, name, resp.Urn, id) + } + + // No matter the outcome, make sure all promises are resolved. + op.complete(err, resp.Urn, string(id), resp.Properties) + + // Signal the completion of this RPC and notify any potential awaiters. + ctx.endRPC() + }() + + outs := make(map[string]*Output) + for k, s := range op.outState { + outs[k] = s.out + } + return &ResourceState{ + URN: (*URNOutput)(op.outURN.out), + ID: (*IDOutput)(op.outID.out), + State: outs, + }, nil } // RegisterResource creates and registers a new resource object. t is the fully qualified type token and name is @@ -188,42 +230,10 @@ func (ctx *Context) RegisterResource( return nil, errors.New("resource name argument (for URN creation) cannot be empty") } - // Get the parent and dependency URNs from the options, in addition to the protection bit. If there wasn't an - // explicit parent, and a root stack resource exists, we will automatically parent to that. - parentURN, optDepURNs, protect := ctx.getOpts(opts...) - - // Serialize all properties, first by awaiting them, and then marshaling them to the requisite gRPC values. - keys, rpcProps, rpcDepURNs, err := marshalInputs(props) + // Prepare the inputs for an impending operation. + op, err := ctx.newResourceOperation(custom, props, opts...) if err != nil { - return nil, errors.Wrap(err, "marshaling properties") - } - - // Merge all dependencies with what we got earlier from property marshaling, and remove duplicates. - var depURNs []string - depMap := make(map[URN]bool) - for _, dep := range append(optDepURNs, rpcDepURNs...) { - if _, has := depMap[dep]; !has { - depURNs = append(depURNs, string(dep)) - depMap[dep] = true - } - } - sort.Strings(depURNs) - - // Create a set of resolvers that we'll use to finalize state, for URNs, IDs, and output properties. - urn, resolveURN, rejectURN := NewOutput(nil) - - var id *Output - var resolveID func(interface{}, bool) - var rejectID func(error) - if custom { - id, resolveID, rejectID = NewOutput(nil) - } - - state := make(map[string]*Output) - resolveState := make(map[string]func(interface{}, bool)) - rejectState := make(map[string]func(error)) - for _, key := range keys { - state[key], resolveState[key], rejectState[key] = NewOutput(nil) + return nil, err } // Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown. @@ -238,70 +248,163 @@ func (ctx *Context) RegisterResource( resp, err := ctx.monitor.RegisterResource(ctx.ctx, &pulumirpc.RegisterResourceRequest{ Type: t, Name: name, - Parent: string(parentURN), - Object: rpcProps, + Parent: op.parent, + Object: op.rpcProps, Custom: custom, - Protect: protect, - Dependencies: depURNs, + Protect: op.protect, + Dependencies: op.deps, }) - var outprops map[string]interface{} - if err == nil { - outprops, err = unmarshalOutputs(resp.Object) - } if err != nil { - // If there was an error, we must reject everything: URN, ID, and state properties. glog.V(9).Infof("RegisterResource(%s, %s): error: %v", t, name, err) - rejectURN(err) - if rejectID != nil { - rejectID(err) - } - for _, reject := range rejectState { - reject(err) - } } else { - glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s %d", t, name, resp.Urn, resp.Id, len(outprops)) - - // Resolve the URN and ID. - resolveURN(URN(resp.Urn), true) - if resolveID != nil { - resolveID(ID(resp.Id), true) - } - - // During previews, it's possible that nils will be returned due to unknown values. This function - // determines the known-ed-ness of a given value below. - isKnown := func(v interface{}) bool { - return !ctx.DryRun() || v != nil - } - - // Now resolve all output properties. - seen := make(map[string]bool) - for key, v := range outprops { - if resolve, has := resolveState[key]; has { - resolve(v, isKnown(v)) - seen[key] = true - } - } - - // If we didn't get back any inputs as outputs, resolve them to the inputs. - for key, resolve := range resolveState { - if !seen[key] { - v := props[key] - resolve(v, isKnown(v)) - } - } + glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s ...", t, name, resp.Urn, resp.Id) } + // No matter the outcome, make sure all promises are resolved. + op.complete(err, resp.Urn, resp.Id, resp.Object) + // Signal the completion of this RPC and notify any potential awaiters. ctx.endRPC() }() + outs := make(map[string]*Output) + for k, s := range op.outState { + outs[k] = s.out + } return &ResourceState{ - URN: urn, - ID: id, - State: state, + URN: (*URNOutput)(op.outURN.out), + ID: (*IDOutput)(op.outID.out), + State: outs, }, nil } +// resourceOperation reflects all of the inputs necessary to perform core resource RPC operations. +type resourceOperation struct { + ctx *Context + parent string + deps []string + protect bool + props map[string]interface{} + rpcProps *structpb.Struct + outURN *resourceOutput + outID *resourceOutput + outState map[string]*resourceOutput +} + +// newResourceOperation prepares the inputs for a resource operation, shared between read and register. +func (ctx *Context) newResourceOperation(custom bool, props map[string]interface{}, + opts ...ResourceOpt) (*resourceOperation, error) { + // Get the parent and dependency URNs from the options, in addition to the protection bit. If there wasn't an + // explicit parent, and a root stack resource exists, we will automatically parent to that. + parent, optDeps, protect := ctx.getOpts(opts...) + + // Serialize all properties, first by awaiting them, and then marshaling them to the requisite gRPC values. + keys, rpcProps, rpcDeps, err := marshalInputs(props) + if err != nil { + return nil, errors.Wrap(err, "marshaling properties") + } + + // Merge all dependencies with what we got earlier from property marshaling, and remove duplicates. + var deps []string + depMap := make(map[URN]bool) + for _, dep := range append(optDeps, rpcDeps...) { + if _, has := depMap[dep]; !has { + deps = append(deps, string(dep)) + depMap[dep] = true + } + } + sort.Strings(deps) + + // Create a set of resolvers that we'll use to finalize state, for URNs, IDs, and output properties. + outURN, resolveURN, rejectURN := NewOutput(nil) + urn := &resourceOutput{out: outURN, resolve: resolveURN, reject: rejectURN} + + var id *resourceOutput + if custom { + outID, resolveID, rejectID := NewOutput(nil) + id = &resourceOutput{out: outID, resolve: resolveID, reject: rejectID} + } + + state := make(map[string]*resourceOutput) + for _, key := range keys { + outState, resolveState, rejectState := NewOutput(nil) + state[key] = &resourceOutput{ + out: outState, + resolve: resolveState, + reject: rejectState, + } + } + + return &resourceOperation{ + ctx: ctx, + parent: string(parent), + deps: deps, + protect: protect, + props: props, + rpcProps: rpcProps, + outURN: urn, + outID: id, + outState: state, + }, nil +} + +// complete finishes a resource operation given the set of RPC results. +func (op *resourceOperation) complete(err error, urn string, id string, result *structpb.Struct) { + var outprops map[string]interface{} + if err == nil { + outprops, err = unmarshalOutputs(result) + } + if err != nil { + // If there was an error, we must reject everything: URN, ID, and state properties. + op.outURN.reject(err) + if op.outID != nil { + op.outID.reject(err) + } + for _, s := range op.outState { + s.reject(err) + } + } else { + // Resolve the URN and ID. + op.outURN.resolve(URN(urn), true) + if op.outID != nil { + if id == "" && op.ctx.DryRun() { + op.outID.resolve("", false) + } else { + op.outID.resolve(ID(id), true) + } + } + + // During previews, it's possible that nils will be returned due to unknown values. This function + // determines the known-ed-ness of a given value below. + isKnown := func(v interface{}) bool { + return !op.ctx.DryRun() || v != nil + } + + // Now resolve all output properties. + seen := make(map[string]bool) + for k, v := range outprops { + if s, has := op.outState[k]; has { + s.resolve(v, isKnown(v)) + seen[k] = true + } + } + + // If we didn't get back any inputs as outputs, resolve them to the inputs. + for k, s := range op.outState { + if !seen[k] { + v := op.props[k] + s.resolve(v, isKnown(v)) + } + } + } +} + +type resourceOutput struct { + out *Output + resolve func(interface{}, bool) + reject func(error) +} + // getOpts returns a set of resource options from an array of them. This includes the parent URN, any // dependency URNs, and a boolean indicating whether the resource is to be protected. func (ctx *Context) getOpts(opts ...ResourceOpt) (URN, []URN, bool) { @@ -389,9 +492,9 @@ func (ctx *Context) waitForRPCs() { // ResourceState contains the results of a resource registration operation. type ResourceState struct { // URN will resolve to the resource's URN after registration has completed. - URN *Output + URN *URNOutput // ID will resolve to the resource's ID after registration, provided this is for a custom resource. - ID *Output + ID *IDOutput // State contains the full set of expected output properties and will resolve after completion. State Outputs } diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 688e4ee28..5e1a3ebae 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -402,6 +402,21 @@ func (out *Float64Output) Apply(applier func(float64) (interface{}, error)) *Out }) } +// IDOutput is an Output that is typed to return ID values. +type IDOutput Output + +// Value returns the underlying number value. +func (out *IDOutput) Value() (ID, bool, error) { + return (*Output)(out).ID() +} + +// Apply applies a transformation to the ID value when it is available. +func (out *IDOutput) Apply(applier func(ID) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(ID(cast.ToString(v))) + }) +} + // IntOutput is an Output that is typed to return int values. type IntOutput Output @@ -579,3 +594,18 @@ func (out *Uint64Output) Apply(applier func(uint64) (interface{}, error)) *Outpu return applier(cast.ToUint64(v)) }) } + +// URNOutput is an Output that is typed to return URN values. +type URNOutput Output + +// Value returns the underlying number value. +func (out *URNOutput) Value() (URN, error) { + return (*Output)(out).URN() +} + +// Apply applies a transformation to the URN value when it is available. +func (out *URNOutput) Apply(applier func(URN) (interface{}, error)) *Output { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(URN(cast.ToString(v))) + }) +} diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go index 8e5ca950b..1f020b5b0 100644 --- a/sdk/go/pulumi/run.go +++ b/sdk/go/pulumi/run.go @@ -69,7 +69,7 @@ func RunErr(body RunFunc) error { if err != nil { return err } - ctx.stackR, err = reg.URN.URN() + ctx.stackR, err = reg.URN.Value() if err != nil { return err } From b28f64316422ca994fae25828e44bab3e200de29 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 09:17:19 -0700 Subject: [PATCH 14/19] Add integration test support for Go This adds integration test support framework for Go. It also adds a test case for the basic empty Pulumi Go program. --- pkg/testing/integration/program.go | 32 ++++++++++++++++++++++++++ sdk/go/pulumi/context.go | 6 ++++- sdk/go/pulumi/run.go | 2 +- tests/integration/empty/go/Pulumi.yaml | 3 +++ tests/integration/empty/go/main.go | 13 +++++++++++ tests/integration/integration_test.go | 8 +++++++ 6 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 tests/integration/empty/go/Pulumi.yaml create mode 100644 tests/integration/empty/go/main.go diff --git a/pkg/testing/integration/program.go b/pkg/testing/integration/program.go index 1e40a8002..8b2e4ca6d 100644 --- a/pkg/testing/integration/program.go +++ b/pkg/testing/integration/program.go @@ -181,6 +181,8 @@ type ProgramTestOptions struct { Bin string // YarnBin is a location of a `yarn` executable to be run. Taken from the $PATH if missing. YarnBin string + // GoBin is a location of a `go` executable to be run. Taken from the $PATH if missing. + GoBin string // Additional environment variaibles to pass for each command we run. Env []string @@ -366,6 +368,7 @@ type programTester struct { opts *ProgramTestOptions // options that control this test run. bin string // the `pulumi` binary we are using. yarnBin string // the `yarn` binary we are using. + goBin string // the `go` binary we are using. } func newProgramTester(t *testing.T, opts *ProgramTestOptions) *programTester { @@ -380,6 +383,10 @@ func (pt *programTester) getYarnBin() (string, error) { return getCmdBin(&pt.yarnBin, "yarn", pt.opts.YarnBin) } +func (pt *programTester) getGoBin() (string, error) { + return getCmdBin(&pt.goBin, "go", pt.opts.GoBin) +} + func (pt *programTester) pulumiCmd(args []string) ([]string, error) { bin, err := pt.getBin() if err != nil { @@ -910,6 +917,8 @@ func (pt *programTester) prepareProject(projectDir string) error { return pt.prepareNodeJSProject(projinfo) case "python": return pt.preparePythonProject(projinfo) + case "go": + return pt.prepareGoProject(projinfo) default: return errors.Errorf("unrecognized project runtime: %s", proj.Runtime) } @@ -952,3 +961,26 @@ func (pt *programTester) prepareNodeJSProject(projinfo *engine.Projinfo) error { func (pt *programTester) preparePythonProject(projinfo *engine.Projinfo) error { return nil } + +// prepareGoProject runs setup necessary to get a Go project ready for `pulumi` commands. +func (pt *programTester) prepareGoProject(projinfo *engine.Projinfo) error { + // Go programs are compiled, so we will compile the project first. + goBin, err := pt.getGoBin() + if err != nil { + return errors.Wrap(err, "locating `go` binary") + } + + // Ensure GOPATH is known. + gopath := os.Getenv("GOPATH") + if gopath == "" { + return errors.New("$GOPATH must be set to test a Go project") + } + + // To compile, simply run `go build -o $GOPATH/bin/ .` from the project's working directory. + cwd, _, err := projinfo.GetPwdMain() + if err != nil { + return err + } + 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/context.go b/sdk/go/pulumi/context.go index 103a2dcf4..bd3591308 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -267,13 +267,17 @@ func (ctx *Context) RegisterResource( ctx.endRPC() }() + var id *IDOutput + if op.outID != nil { + id = (*IDOutput)(op.outID.out) + } outs := make(map[string]*Output) for k, s := range op.outState { outs[k] = s.out } return &ResourceState{ URN: (*URNOutput)(op.outURN.out), - ID: (*IDOutput)(op.outID.out), + ID: id, State: outs, }, nil } diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go index 1f020b5b0..c45a0e124 100644 --- a/sdk/go/pulumi/run.go +++ b/sdk/go/pulumi/run.go @@ -47,7 +47,7 @@ func RunErr(body RunFunc) error { // Validate some properties. if info.Project == "" { - return errors.New("missing project name") + return errors.Errorf("missing project name (%s)", os.Environ()) } else if info.Stack == "" { return errors.New("missing stack name") } else if info.MonitorAddr == "" { diff --git a/tests/integration/empty/go/Pulumi.yaml b/tests/integration/empty/go/Pulumi.yaml new file mode 100644 index 000000000..160eaee46 --- /dev/null +++ b/tests/integration/empty/go/Pulumi.yaml @@ -0,0 +1,3 @@ +name: emptygo +description: An empty Go Pulumi program. +runtime: go diff --git a/tests/integration/empty/go/main.go b/tests/integration/empty/go/main.go new file mode 100644 index 000000000..2c9eb9065 --- /dev/null +++ b/tests/integration/empty/go/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 a0b3eb2dc..b226c3b8f 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -35,6 +35,14 @@ func TestEmptyPython(t *testing.T) { }) } +// TestEmptyGo simply tests that we can run an empty Go project. +func TestEmptyGo(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("empty", "go"), + Quick: true, + }) +} + // TestProjectMain tests out the ability to override the main entrypoint. func TestProjectMain(t *testing.T) { var test integration.ProgramTestOptions From b19ecd6602b138f0054478782f0f84cbfdb6bcb2 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 09:24:46 -0700 Subject: [PATCH 15/19] Add a basic Go configuration integration test --- sdk/go/pulumi/config/config.go | 2 +- sdk/go/pulumi/config/config_test.go | 2 +- sdk/go/pulumi/config/get.go | 2 +- sdk/go/pulumi/config/require.go | 2 +- sdk/go/pulumi/config/try.go | 2 +- sdk/go/pulumi/run.go | 2 +- tests/integration/config_basic/go/Pulumi.yaml | 3 ++ tests/integration/config_basic/go/main.go | 30 +++++++++++++++++++ tests/integration/integration_test.go | 14 +++++++++ 9 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 tests/integration/config_basic/go/Pulumi.yaml create mode 100644 tests/integration/config_basic/go/main.go diff --git a/sdk/go/pulumi/config/config.go b/sdk/go/pulumi/config/config.go index 2ad374a38..5be3663de 100644 --- a/sdk/go/pulumi/config/config.go +++ b/sdk/go/pulumi/config/config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pulumi +package config import ( "github.com/pulumi/pulumi/sdk/go/pulumi" diff --git a/sdk/go/pulumi/config/config_test.go b/sdk/go/pulumi/config/config_test.go index 786543104..11fbb0cc8 100644 --- a/sdk/go/pulumi/config/config_test.go +++ b/sdk/go/pulumi/config/config_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pulumi +package config import ( "context" diff --git a/sdk/go/pulumi/config/get.go b/sdk/go/pulumi/config/get.go index e5ab5c592..631625c88 100644 --- a/sdk/go/pulumi/config/get.go +++ b/sdk/go/pulumi/config/get.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pulumi +package config import ( "github.com/spf13/cast" diff --git a/sdk/go/pulumi/config/require.go b/sdk/go/pulumi/config/require.go index a69350f76..9d06b31ab 100644 --- a/sdk/go/pulumi/config/require.go +++ b/sdk/go/pulumi/config/require.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pulumi +package config import ( "github.com/spf13/cast" diff --git a/sdk/go/pulumi/config/try.go b/sdk/go/pulumi/config/try.go index 0b5ed1e4a..a9cb55b8c 100644 --- a/sdk/go/pulumi/config/try.go +++ b/sdk/go/pulumi/config/try.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pulumi +package config import ( "github.com/pkg/errors" diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go index c45a0e124..457f428f1 100644 --- a/sdk/go/pulumi/run.go +++ b/sdk/go/pulumi/run.go @@ -47,7 +47,7 @@ func RunErr(body RunFunc) error { // Validate some properties. if info.Project == "" { - return errors.Errorf("missing project name (%s)", os.Environ()) + return errors.Errorf("missing project name") } else if info.Stack == "" { return errors.New("missing stack name") } else if info.MonitorAddr == "" { diff --git a/tests/integration/config_basic/go/Pulumi.yaml b/tests/integration/config_basic/go/Pulumi.yaml new file mode 100644 index 000000000..c9e3d4852 --- /dev/null +++ b/tests/integration/config_basic/go/Pulumi.yaml @@ -0,0 +1,3 @@ +name: config_basic_go +runtime: go +description: A simple Go program that uses configuration. diff --git a/tests/integration/config_basic/go/main.go b/tests/integration/config_basic/go/main.go new file mode 100644 index 000000000..c73bcd7e3 --- /dev/null +++ b/tests/integration/config_basic/go/main.go @@ -0,0 +1,30 @@ +// Copyright 2016-2018, Pulumi Corporation. All rights reserved. + +package main + +import ( + "fmt" + "github.com/pulumi/pulumi/sdk/go/pulumi" + "github.com/pulumi/pulumi/sdk/go/pulumi/config" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + // Just test that basic config works. + cfg := config.New(ctx, "config_basic_go") + + // This value is plaintext and doesn't require encryption. + value := cfg.Require("aConfigValue") + if value != "this value is a value" { + return fmt.Errorf("aConfigValue not the expected value; got %s", value) + } + + // This value is a secret and is encrypted using the passphrase `supersecret`. + secret := cfg.Require("bEncryptedSecret") + if secret != "this super secret is encrypted" { + return fmt.Errorf("bEncryptedSecret not the expected value; got %s", secret) + } + + return nil + }) +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index b226c3b8f..0eb2cf190 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -364,3 +364,17 @@ func TestConfigBasicPython(t *testing.T) { }, }) } + +// Tests basic configuration from the perspective of a Pulumi Go program. +func TestConfigBasicGo(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("config_basic", "go"), + Quick: true, + Config: map[string]string{ + "aConfigValue": "this value is a value", + }, + Secrets: map[string]string{ + "bEncryptedSecret": "this super secret is encrypted", + }, + }) +} From 48ddf5b3b022853805ab351ebc38c602c2df41a0 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 11:45:24 -0700 Subject: [PATCH 16/19] Fix a few things 1) Use a state block for *Outputs, just to protect against dereferencing and aliasing. These are mutable due to concurrency. 2) Dig into *Output type aliases, like *URNOutput, et. al, during RPC marshaling. --- sdk/go/pulumi/properties.go | 27 ++++++++++++------- sdk/go/pulumi/rpc.go | 52 +++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 5e1a3ebae..641dc80f1 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -25,6 +25,11 @@ import ( // resources, allowing that new resource to know both the value as well as the resource the value came from. This // allows for a precise "dependency graph" to be created, which properly tracks the relationship between resources. type Output struct { + s *outputState // protect against value aliasing. +} + +// outputState is a heap-allocated block of state for each output property, in case of aliasing. +type outputState struct { sync chan *valueOrError // the channel for outputs whose values are not yet known. voe *valueOrError // the value or error, after the channel has been rendezvoused with. deps []Resource // the dependencies associated with this output property. @@ -42,8 +47,10 @@ type valueOrError struct { // error; exactly one function must be called. This acts like a promise. func NewOutput(deps []Resource) (*Output, func(interface{}, bool), func(error)) { out := &Output{ - sync: make(chan *valueOrError, 1), - deps: deps, + s: &outputState{ + sync: make(chan *valueOrError, 1), + deps: deps, + }, } return out, out.resolve, out.reject } @@ -62,14 +69,14 @@ func (out *Output) resolve(v interface{}, known bool) { } }() } else { - out.sync <- &valueOrError{value: v, known: known} + out.s.sync <- &valueOrError{value: v, known: known} } } // reject will reject the output. It is not exported, because we want to control the capabilities tightly, such // that anybody who happens to have an Output is not allowed to reject it; only those who created it can. func (out *Output) reject(err error) { - out.sync <- &valueOrError{err: err} + out.s.sync <- &valueOrError{err: err} } // Apply transforms the data of the output property using the applier func. The result remains an output property, @@ -114,19 +121,19 @@ func (out *Output) Apply(applier func(v interface{}) (interface{}, error)) *Outp } // Deps returns the dependencies for this output property. -func (out *Output) Deps() []Resource { return out.deps } +func (out *Output) Deps() []Resource { return out.s.deps } // Value retrieves the underlying value for this output property. func (out *Output) Value() (interface{}, bool, error) { // If neither error nor value are available, first await the channel. Only one Goroutine will make it through this // and is responsible for closing the channel, to signal to other awaiters that it's safe to read the values. - if out.voe == nil { - if voe := <-out.sync; voe != nil { - out.voe = voe - close(out.sync) + if out.s.voe == nil { + if voe := <-out.s.sync; voe != nil { + out.s.voe = voe + close(out.s.sync) } } - return out.voe.value, out.voe.known, out.voe.err + return out.s.voe.value, out.s.voe.known, out.s.voe.err } // Archive retrives the underlying value for this output property as an archive. diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go index 1f78bda00..a48554562 100644 --- a/sdk/go/pulumi/rpc.go +++ b/sdk/go/pulumi/rpc.go @@ -105,24 +105,10 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { "path": t.Path(), "uri": t.URI(), }, nil, nil + case Output: + return marshalInputOutput(&t) case *Output: - // Await the value and return its raw value. - ov, known, err := t.Value() - if err != nil { - return nil, nil, err - } - - // If the value is known, marshal it. - if known { - e, d, merr := marshalInput(ov) - if merr != nil { - return nil, nil, merr - } - return e, append(t.Deps(), d...), nil - } - - // Otherwise, simply return the unknown value sentinel. - return rpcTokenUnknownValue, t.Deps(), nil + return marshalInputOutput(t) case CustomResource: // Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a e, d, err := marshalInput(t.ID()) @@ -170,11 +156,19 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { } return obj, deps, nil case reflect.Ptr: - // For pointers, recurse into the underlying value. + // See if this is an alias for *Output. If so, convert to an *Output, and recurse. + e := rv.Elem() + ot := reflect.TypeOf(Output{}) + if e.Type().ConvertibleTo(ot) { + oo := e.Convert(ot) + return marshalInput(oo.Interface()) + } + + // For all other pointers, recurse into the underlying value. if rv.IsNil() { return nil, nil, nil } - return marshalInput(rv.Interface()) + return marshalInput(e.Interface()) case reflect.String: return marshalInput(rv.String()) } @@ -182,6 +176,26 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { return nil, nil, errors.Errorf("unrecognized input property type: %v (%v)", v, reflect.TypeOf(v)) } +func marshalInputOutput(out *Output) (interface{}, []Resource, error) { + // Await the value and return its raw value. + ov, known, err := out.Value() + if err != nil { + return nil, nil, err + } + + // If the value is known, marshal it. + if known { + e, d, merr := marshalInput(ov) + if merr != nil { + return nil, nil, merr + } + return e, append(out.Deps(), d...), nil + } + + // Otherwise, simply return the unknown value sentinel. + return rpcTokenUnknownValue, out.Deps(), nil +} + // unmarshalOutputs unmarshals all the outputs into a simple map. func unmarshalOutputs(outs *structpb.Struct) (map[string]interface{}, error) { outprops, err := plugin.UnmarshalProperties(outs, plugin.MarshalOptions{}) From f1aec12df2ae3a4056eb9c04fc2a66439dcb43f0 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 11:54:11 -0700 Subject: [PATCH 17/19] Avoid aliasing *Output when possible --- sdk/go/pulumi/rpc.go | 9 ++++----- sdk/go/pulumi/run.go | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go index a48554562..32955cc83 100644 --- a/sdk/go/pulumi/rpc.go +++ b/sdk/go/pulumi/rpc.go @@ -157,10 +157,9 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { return obj, deps, nil case reflect.Ptr: // See if this is an alias for *Output. If so, convert to an *Output, and recurse. - e := rv.Elem() - ot := reflect.TypeOf(Output{}) - if e.Type().ConvertibleTo(ot) { - oo := e.Convert(ot) + ot := reflect.TypeOf(&Output{}) + if rv.Type().ConvertibleTo(ot) { + oo := rv.Convert(ot) return marshalInput(oo.Interface()) } @@ -168,7 +167,7 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { if rv.IsNil() { return nil, nil, nil } - return marshalInput(e.Interface()) + return marshalInput(rv.Elem().Interface()) case reflect.String: return marshalInput(rv.String()) } diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go index 457f428f1..fdb9c1a0c 100644 --- a/sdk/go/pulumi/run.go +++ b/sdk/go/pulumi/run.go @@ -73,6 +73,7 @@ func RunErr(body RunFunc) error { if err != nil { return err } + contract.Assertf(ctx.stackR != "", "expected root stack resource to have a non-empty URN") // Execute the body. var result error From 906d2fd2e02c27e1ad82a141c36cb501144d0b62 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 12:09:45 -0700 Subject: [PATCH 18/19] Support string type aliases --- sdk/go/pulumi/properties.go | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 641dc80f1..eab88a951 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -15,6 +15,8 @@ package pulumi import ( + "reflect" + "github.com/spf13/cast" "github.com/pulumi/pulumi/sdk/go/pulumi/asset" @@ -129,8 +131,8 @@ func (out *Output) Value() (interface{}, bool, error) { // and is responsible for closing the channel, to signal to other awaiters that it's safe to read the values. if out.s.voe == nil { if voe := <-out.s.sync; voe != nil { - out.s.voe = voe - close(out.s.sync) + out.s.voe = voe // first time through, publish the value. + close(out.s.sync) // and close the channel to signal to others that the memozied value is available. } } return out.s.voe.value, out.s.voe.known, out.s.voe.err @@ -205,7 +207,7 @@ func (out *Output) ID() (ID, bool, error) { if err != nil || !known { return "", known, err } - return ID(cast.ToString(v)), true, nil + return ID(toString(v)), true, nil } // Int retrives the underlying value for this output property as a int. @@ -259,7 +261,7 @@ func (out *Output) String() (string, bool, error) { if err != nil || !known { return "", known, err } - return cast.ToString(v), true, nil + return toString(v), true, nil } // Uint retrives the underlying value for this output property as a uint. @@ -313,7 +315,7 @@ func (out *Output) URN() (URN, error) { if err != nil || !known { return "", err } - return URN(cast.ToString(v)), nil + return URN(toString(v)), nil } // Outputs is a map of property name to value, one for each resource output property. @@ -420,7 +422,7 @@ func (out *IDOutput) Value() (ID, bool, error) { // Apply applies a transformation to the ID value when it is available. func (out *IDOutput) Apply(applier func(ID) (interface{}, error)) *Output { return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { - return applier(ID(cast.ToString(v))) + return applier(ID(toString(v))) }) } @@ -523,7 +525,7 @@ func (out *StringOutput) Value() (string, bool, error) { // Apply applies a transformation to the number value when it is available. func (out *StringOutput) Apply(applier func(string) (interface{}, error)) *Output { return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { - return applier(cast.ToString(v)) + return applier(toString(v)) }) } @@ -613,6 +615,22 @@ func (out *URNOutput) Value() (URN, error) { // Apply applies a transformation to the URN value when it is available. func (out *URNOutput) Apply(applier func(URN) (interface{}, error)) *Output { return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { - return applier(URN(cast.ToString(v))) + return applier(URN(toString(v))) }) } + +// toString attempts to convert v to a string. +func toString(v interface{}) string { + if s := cast.ToString(v); s != "" { + return "" + } + + // See if this can convert through reflection (e.g., for type aliases). + st := reflect.TypeOf("") + sv := reflect.ValueOf(v) + if sv.Type().ConvertibleTo(st) { + return sv.Convert(st).Interface().(string) + } + + return "" +} From 5460520b5305d28af9332c42a2d25a2d0cfa9f09 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 10 Jun 2018 12:36:14 -0700 Subject: [PATCH 19/19] Fix a *facepalm* --- sdk/go/pulumi/properties.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index eab88a951..6400052b4 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -622,7 +622,7 @@ func (out *URNOutput) Apply(applier func(URN) (interface{}, error)) *Output { // toString attempts to convert v to a string. func toString(v interface{}) string { if s := cast.ToString(v); s != "" { - return "" + return s } // See if this can convert through reflection (e.g., for type aliases).