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/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/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/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/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/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..f9f4b53d6 --- /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 + $(GOMETALINTER) ./pulumi-language-go/... | sort + +test_fast:: + go test -cover -parallel ${TESTPARALLELISM} ${PROJECT_PKGS} 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..7c3de3369 --- /dev/null +++ b/sdk/go/pulumi-language-go/main.go @@ -0,0 +1,180 @@ +// 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..f7c2ff432 --- /dev/null +++ b/sdk/go/pulumi/asset/asset.go @@ -0,0 +1,110 @@ +// 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 +} + +// 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 } + +// 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 { + // 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} +} + +// 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 } + +// 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/config/config.go b/sdk/go/pulumi/config/config.go new file mode 100644 index 000000000..5be3663de --- /dev/null +++ b/sdk/go/pulumi/config/config.go @@ -0,0 +1,247 @@ +// 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 config + +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..11fbb0cc8 --- /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 config + +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..631625c88 --- /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 config + +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..9d06b31ab --- /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 config + +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 +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// 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 new file mode 100644 index 000000000..a9cb55b8c --- /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 config + +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 +} + +// 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 { + return false, err + } + return cast.ToBool(v), nil +} + +// 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 { + return 0, err + } + return cast.ToFloat32(v), nil +} + +// 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 { + return 0, err + } + return cast.ToFloat64(v), nil +} + +// 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 { + return 0, err + } + return cast.ToInt(v), nil +} + +// 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 { + return 0, err + } + return cast.ToInt8(v), nil +} + +// 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 { + return 0, err + } + return cast.ToInt16(v), nil +} + +// 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 { + return 0, err + } + return cast.ToInt32(v), nil +} + +// 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 { + return 0, err + } + return cast.ToInt64(v), nil +} + +// 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 { + return 0, err + } + return cast.ToUint(v), nil +} + +// 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 { + return 0, err + } + return cast.ToUint8(v), nil +} + +// 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 { + return 0, err + } + return cast.ToUint16(v), nil +} + +// 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 { + return 0, err + } + return cast.ToUint32(v), nil +} + +// 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 { + return 0, err + } + return cast.ToUint64(v), nil +} diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go new file mode 100644 index 000000000..bd3591308 --- /dev/null +++ b/sdk/go/pulumi/context.go @@ -0,0 +1,514 @@ +// 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" + structpb "github.com/golang/protobuf/ptypes/struct" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + "golang.org/x/net/context" + "google.golang.org/grpc" + + 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 map[string]interface{} + 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) { + // 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) + } + + 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{} + return &Context{ + ctx: ctx, + info: info, + exports: make(map[string]interface{}), + monitorConn: monitorConn, + monitor: monitor, + engineConn: engineConn, + engine: engine, + 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 ctx.engineConn != nil { + if err := ctx.engineConn.Close(); err != nil { + return err + } + } + if ctx.monitorConn != nil { + 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 } + +// 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. 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 +// 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, props map[string]interface{}, 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") + } + + // 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 +// 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 map[string]interface{}, 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") + } + + // Prepare the inputs for an impending operation. + op, err := ctx.newResourceOperation(custom, 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 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: op.parent, + Object: op.rpcProps, + Custom: custom, + Protect: op.protect, + Dependencies: op.deps, + }) + 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, 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() + }() + + 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: id, + 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) { + 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 *URNOutput + // ID will resolve to the resource's ID after registration, provided this is for a custom resource. + ID *IDOutput + // 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 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 interface{}) { + ctx.exports[name] = value +} diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go new file mode 100644 index 000000000..6400052b4 --- /dev/null +++ b/sdk/go/pulumi/properties.go @@ -0,0 +1,636 @@ +// 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" + + "github.com/spf13/cast" + + "github.com/pulumi/pulumi/sdk/go/pulumi/asset" +) + +// 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 { + 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. +} + +// 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. + 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{}, bool), func(error)) { + out := &Output{ + s: &outputState{ + sync: make(chan *valueOrError, 1), + deps: deps, + }, + } + 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{}, known bool) { + // If v is another output, chain this rather than resolving to an output directly. + if other, isOut := v.(*Output); known && isOut { + go func() { + real, otherKnown, err := other.Value() + if err != nil { + out.reject(err) + } else { + out.resolve(real, otherKnown) + } + }() + } else { + 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.s.sync <- &valueOrError{err: err} +} + +// 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, known, err := out.Value() + if err != nil { + reject(err) + break + } else { + 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 + } + } + } + }() + return result +} + +// Deps returns the dependencies for this output property. +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.s.voe == nil { + if voe := <-out.s.sync; voe != nil { + 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 +} + +// Archive retrives the underlying value for this output property as an archive. +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), true, nil +} + +// Array retrives the underlying value for this output property as an array. +func (out *Output) Array() ([]interface{}, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err + } + return cast.ToSlice(v), true, nil +} + +// Asset retrives the underlying value for this output property as an asset. +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), true, nil +} + +// Bool retrives the underlying value for this output property as a bool. +func (out *Output) Bool() (bool, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return false, known, err + } + 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{}, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err + } + return cast.ToStringMap(v), true, nil +} + +// Float32 retrives the underlying value for this output property as a float32. +func (out *Output) Float32() (float32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToFloat32(v), true, nil +} + +// Float64 retrives the underlying value for this output property as a float64. +func (out *Output) Float64() (float64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToFloat64(v), true, nil +} + +// ID retrives the underlying value for this output property as an ID. +func (out *Output) ID() (ID, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return "", known, err + } + return ID(toString(v)), true, nil +} + +// Int retrives the underlying value for this output property as a int. +func (out *Output) Int() (int, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToInt(v), true, nil +} + +// Int8 retrives the underlying value for this output property as a int8. +func (out *Output) Int8() (int8, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToInt8(v), true, nil +} + +// Int16 retrives the underlying value for this output property as a int16. +func (out *Output) Int16() (int16, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToInt16(v), true, nil +} + +// Int32 retrives the underlying value for this output property as a int32. +func (out *Output) Int32() (int32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToInt32(v), true, nil +} + +// Int64 retrives the underlying value for this output property as a int64. +func (out *Output) Int64() (int64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToInt64(v), true, nil +} + +// String retrives the underlying value for this output property as a string. +func (out *Output) String() (string, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return "", known, err + } + return toString(v), true, nil +} + +// Uint retrives the underlying value for this output property as a uint. +func (out *Output) Uint() (uint, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToUint(v), true, nil +} + +// Uint8 retrives the underlying value for this output property as a uint8. +func (out *Output) Uint8() (uint8, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToUint8(v), true, nil +} + +// Uint16 retrives the underlying value for this output property as a uint16. +func (out *Output) Uint16() (uint16, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToUint16(v), true, nil +} + +// Uint32 retrives the underlying value for this output property as a uint32. +func (out *Output) Uint32() (uint32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + return cast.ToUint32(v), true, nil +} + +// Uint64 retrives the underlying value for this output property as a uint64. +func (out *Output) Uint64() (uint64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err + } + 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, known, err := out.Value() + if err != nil || !known { + return "", err + } + return URN(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, 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 { + 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{}, 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 { + 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, 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 { + 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, 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 { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(v.(bool)) + }) +} + +// Float32Output is an Output that is typed to return float32 values. +type Float32Output Output + +// Value returns the underlying number value. +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 { + 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, 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 { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(cast.ToFloat64(v)) + }) +} + +// 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(toString(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, 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 { + 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, 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 { + 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, 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 { + 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, 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 { + 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, 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 { + 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{}, 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 { + 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, 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 { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + return applier(toString(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, 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 { + 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, 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 { + 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, 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 { + 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, 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 { + 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, 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 { + return (*Output)(out).Apply(func(v interface{}) (interface{}, error) { + 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(toString(v))) + }) +} + +// toString attempts to convert v to a string. +func toString(v interface{}) string { + if s := cast.ToString(v); s != "" { + return s + } + + // 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 "" +} diff --git a/sdk/go/pulumi/properties_test.go b/sdk/go/pulumi/properties_test.go new file mode 100644 index 000000000..a2af8b46f --- /dev/null +++ b/sdk/go/pulumi/properties_test.go @@ -0,0 +1,277 @@ +// 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, true) + }() + v, known, err := out.Value() + assert.Nil(t, err) + assert.True(t, known) + 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"}, true) + }() + { + 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]) + 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, true) + }() + { + v, known, err := out.Bool() + assert.Nil(t, err) + assert.True(t, known) + assert.True(t, v) + } + { + b := (*BoolOutput)(out) + v, known, err := b.Value() + assert.Nil(t, err) + assert.True(t, known) + 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", + }, true) + }() + { + 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"]) + assert.Equal(t, "abc", v["z"]) + } + { + b := (*MapOutput)(out) + 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"]) + assert.Equal(t, "abc", v["z"]) + } +} + +func TestNumberOutputs(t *testing.T) { + out, resolve, _ := NewOutput(nil) + go func() { + resolve(42.345, true) + }() + { + v, known, err := out.Float64() + assert.Nil(t, err) + assert.True(t, known) + assert.Equal(t, 42.345, v) + } + { + b := (*Float64Output)(out) + v, known, err := b.Value() + assert.Nil(t, err) + assert.True(t, known) + assert.Equal(t, 42.345, v) + } +} + +func TestStringOutputs(t *testing.T) { + out, resolve, _ := NewOutput(nil) + go func() { + resolve("a stringy output", true) + }() + { + v, known, err := out.String() + assert.Nil(t, err) + assert.True(t, known) + assert.Equal(t, "a stringy output", v) + } + { + b := (*StringOutput)(out) + v, known, err := b.Value() + assert.Nil(t, err) + assert.True(t, known) + 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, true) + go func() { resolveOther(99, true) }() + }() + 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. + { + out, resolve, _ := NewOutput(nil) + go func() { + other, _, rejectOther := NewOutput(nil) + resolve(other, true) + 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, true) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + ranApp = true + return v + 1, nil + }) + 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) + 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, true) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + other, resolveOther, _ := NewOutput(nil) + go func() { resolveOther(v+1, true) }() + ranApp = true + return other, nil + }) + 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, true) }() + 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) + } +} 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..32955cc83 --- /dev/null +++ b/sdk/go/pulumi/rpc.go @@ -0,0 +1,289 @@ +// 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/spf13/cast" + + "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 map[string]interface{}) ([]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 ( + // nolint: gas, linter thinks these are creds, but they aren't. + 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, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64, string: + return t, nil, 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: + return marshalInputOutput(&t) + case *Output: + 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()) + if err != nil { + return nil, nil, err + } + return e, append([]Resource{t}, d...), nil + } + + // 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. + obj := make(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) + mv, d, err := marshalInput(value.Interface()) + if err != nil { + return nil, nil, err + } + + obj[k] = mv + deps = append(deps, d...) + } + return obj, deps, nil + case reflect.Ptr: + // See if this is an alias for *Output. If so, convert to an *Output, and recurse. + ot := reflect.TypeOf(&Output{}) + if rv.Type().ConvertibleTo(ot) { + oo := rv.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.Elem().Interface()) + case reflect.String: + return marshalInput(rv.String()) + } + + 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{}) + 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) { + // 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 { + 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. + 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) + mv, err := unmarshalOutput(value) + if err != nil { + return nil, err + } + + obj[k] = mv + } + 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..11e83769d --- /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", true) + 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"]) + } + } + } +} diff --git a/sdk/go/pulumi/run.go b/sdk/go/pulumi/run.go new file mode 100644 index 000000000..fdb9c1a0c --- /dev/null +++ b/sdk/go/pulumi/run.go @@ -0,0 +1,148 @@ +// 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" + "github.com/pkg/errors" + "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 +// 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) + } +} + +// 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 + // 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() + + // Validate some properties. + if info.Project == "" { + return errors.Errorf("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 { + return err + } + defer contract.IgnoreClose(ctx) + + // 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.Value() + 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 + 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 result +} + +// 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" +) 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/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..0eb2cf190 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 @@ -356,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", + }, + }) +}