Merge pull request #1456 from pulumi/golang

Support Pulumi programs written in Go
This commit is contained in:
Joe Duffy 2018-06-10 13:01:37 -07:00 committed by GitHub
commit e772aac3c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 3167 additions and 3 deletions

8
Gopkg.lock generated
View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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/<projname> .` 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)
}

View file

@ -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/

24
sdk/go/Makefile Normal file
View file

@ -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}

16
sdk/go/README.md Normal file
View file

@ -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: <my-project>
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, `<my-project>`, 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`.

View file

@ -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
}

View file

@ -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 }

View file

@ -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))
}

View file

@ -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)
}

131
sdk/go/pulumi/config/get.go Normal file
View file

@ -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
}

View file

@ -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)
}

149
sdk/go/pulumi/config/try.go Normal file
View file

@ -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
}

514
sdk/go/pulumi/context.go Normal file
View file

@ -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
}

636
sdk/go/pulumi/properties.go Normal file
View file

@ -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 ""
}

View file

@ -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)
}
}

54
sdk/go/pulumi/resource.go Normal file
View file

@ -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
}

289
sdk/go/pulumi/rpc.go Normal file
View file

@ -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
}

88
sdk/go/pulumi/rpc_test.go Normal file
View file

@ -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"])
}
}
}
}

148
sdk/go/pulumi/run.go Normal file
View file

@ -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"
)

View file

@ -0,0 +1,3 @@
name: config_basic_go
runtime: go
description: A simple Go program that uses configuration.

View file

@ -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
})
}

View file

@ -0,0 +1,3 @@
name: emptygo
description: An empty Go Pulumi program.
runtime: go

View file

@ -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
})
}

View file

@ -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",
},
})
}