pulumi/pkg/resource/plugin/provider_plugin.go
Pat Gavlin f848090479 Return all computed inputs from Provider.Check.
As documented in issue #616, the inputs/defaults/outputs model we have
today has fundamental problems. The crux of the issue is that our
current design requires that defaults present in the old state of a
resource are applied to the new inputs for that resource.
Unfortunately, it is not possible for the engine to decide which
defaults remain applicable and which do not; only the provider has that
knowledge.

These changes take a more tactical approach to resolving this issue than
that originally proposed in #616 that avoids breaking compatibility with
existing checkpoints. Rather than treating the Pulumi inputs as the
provider input properties for a resource, these inputs are first
translated by `Check`. In order to accommodate provider defaults that
were chosen for the old resource but should not change for the new,
`Check` now takes the old provider inputs as well as the new Pulumi
inputs. Rather than the Pulumi inputs and provider defaults, the
provider inputs returned by `Check` are recorded in the checkpoint file.

Put simply, these changes remove defaults as a first-class concept
(except inasmuch as is required to retain the ability to read old
checkpoint files) and move the responsibilty for manging and
merging defaults into the provider that supplies them.

Fixes #616.
2017-12-03 09:33:16 -08:00

312 lines
10 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package plugin
import (
"fmt"
"strings"
"github.com/golang/glog"
pbempty "github.com/golang/protobuf/ptypes/empty"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
)
const ProviderPluginPrefix = "pulumi-provider-"
// provider reflects a resource plugin, loaded dynamically for a single package.
type provider struct {
ctx *Context
pkg tokens.Package
plug *plugin
client pulumirpc.ResourceProviderClient
}
// NewProvider attempts to bind to a given package's resource plugin and then creates a gRPC connection to it. If the
// plugin could not be found, or an error occurs while creating the child process, an error is returned.
func NewProvider(host Host, ctx *Context, pkg tokens.Package) (Provider, error) {
// Go ahead and attempt to load the plugin from the PATH.
srvexe := ProviderPluginPrefix + strings.Replace(string(pkg), tokens.QNameDelimiter, "_", -1)
plug, err := newPlugin(ctx, srvexe, fmt.Sprintf("%v (resource)", pkg), []string{host.ServerAddr()})
if err != nil {
return nil, err
} else if plug == nil {
return nil, nil
}
return &provider{
ctx: ctx,
pkg: pkg,
plug: plug,
client: pulumirpc.NewResourceProviderClient(plug.Conn),
}, nil
}
func (p *provider) Pkg() tokens.Package { return p.pkg }
// Configure configures the resource provider with "globals" that control its behavior.
func (p *provider) Configure(vars map[tokens.ModuleMember]string) error {
glog.V(7).Infof("resource[%v].Configure(#vars=%v) executing", p.pkg, len(vars))
config := make(map[string]string)
for k, v := range vars {
config[string(k)] = v
}
_, err := p.client.Configure(p.ctx.Request(), &pulumirpc.ConfigureRequest{Variables: config})
if err != nil {
glog.V(7).Infof("resource[%v].Configure(#vars=%v,...) failed: err=%v", p.pkg, len(vars), err)
return err
}
return nil
}
// Check validates that the given property bag is valid for a resource of the given type.
func (p *provider) Check(urn resource.URN,
olds, news resource.PropertyMap) (resource.PropertyMap, []CheckFailure, error) {
glog.V(7).Infof("resource[%v].Check(urn=%v,#olds=%v,#news=%v) executing", p.pkg, urn, len(olds), len(news))
molds, err := MarshalProperties(olds, MarshalOptions{KeepUnknowns: true})
if err != nil {
return nil, nil, err
}
mnews, err := MarshalProperties(news, MarshalOptions{KeepUnknowns: true})
if err != nil {
return nil, nil, err
}
resp, err := p.client.Check(p.ctx.Request(), &pulumirpc.CheckRequest{
Urn: string(urn),
Olds: molds,
News: mnews,
})
if err != nil {
glog.V(7).Infof("resource[%v].Check(urn=%v,...) failed: err=%v", p.pkg, urn, err)
return nil, nil, err
}
// Unmarshal the provider inputs.
var inputs resource.PropertyMap
if ins := resp.GetInputs(); ins != nil {
inputs, err = UnmarshalProperties(ins, MarshalOptions{KeepUnknowns: true})
if err != nil {
return nil, nil, err
}
}
// And now any properties that failed verification.
var failures []CheckFailure
for _, failure := range resp.GetFailures() {
failures = append(failures, CheckFailure{resource.PropertyKey(failure.Property), failure.Reason})
}
glog.V(7).Infof("resource[%v].Check(urn=%v,...) success: inputs=#%v failures=#%v",
p.pkg, urn, len(inputs), len(failures))
return inputs, failures, nil
}
// Diff checks what impacts a hypothetical update will have on the resource's properties.
func (p *provider) Diff(urn resource.URN, id resource.ID,
olds resource.PropertyMap, news resource.PropertyMap) (DiffResult, error) {
contract.Assert(urn != "")
contract.Assert(id != "")
contract.Assert(news != nil)
contract.Assert(olds != nil)
glog.V(7).Infof("resource[%v].Diff(id=%v,urn=%v,#olds=%v,#news=%v) executing",
p.pkg, id, urn, len(olds), len(news))
molds, err := MarshalProperties(olds, MarshalOptions{ElideAssetContents: true})
if err != nil {
return DiffResult{}, err
}
mnews, err := MarshalProperties(news, MarshalOptions{KeepUnknowns: true})
if err != nil {
return DiffResult{}, err
}
resp, err := p.client.Diff(p.ctx.Request(), &pulumirpc.DiffRequest{
Id: string(id),
Urn: string(urn),
Olds: molds,
News: mnews,
})
if err != nil {
glog.V(7).Infof("resource[%v].Diff(id=%v,urn=%v,...) failed: %v", p.pkg, id, urn, err)
return DiffResult{}, err
}
var replaces []resource.PropertyKey
for _, replace := range resp.GetReplaces() {
replaces = append(replaces, resource.PropertyKey(replace))
}
var stables []resource.PropertyKey
for _, stable := range resp.GetStables() {
stables = append(stables, resource.PropertyKey(stable))
}
glog.V(7).Infof("resource[%v].Update(id=%v,urn=%v,...) success: #replaces=%v #stables=%v",
p.pkg, id, urn, len(replaces), len(stables))
return DiffResult{
ReplaceKeys: replaces,
StableKeys: stables,
}, nil
}
// Create allocates a new instance of the provided resource and assigns its unique resource.ID and outputs afterwards.
func (p *provider) Create(urn resource.URN, props resource.PropertyMap) (resource.ID,
resource.PropertyMap, resource.Status, error) {
contract.Assert(urn != "")
contract.Assert(props != nil)
glog.V(7).Infof("resource[%v].Create(urn=%v,#props=%v) executing", p.pkg, urn, len(props))
mprops, err := MarshalProperties(props, MarshalOptions{})
if err != nil {
return "", nil, resource.StatusOK, err
}
resp, err := p.client.Create(p.ctx.Request(), &pulumirpc.CreateRequest{
Urn: string(urn),
Properties: mprops,
})
if err != nil {
glog.V(7).Infof("resource[%v].Create(urn=%v,...) failed: err=%v", p.pkg, urn, err)
return "", nil, resource.StatusUnknown, err
}
id := resource.ID(resp.GetId())
if id == "" {
return "", nil, resource.StatusUnknown,
errors.Errorf("plugin for package '%v' returned empty resource.ID from create '%v'", p.pkg, urn)
}
outs, err := UnmarshalProperties(resp.GetProperties(), MarshalOptions{})
if err != nil {
return "", nil, resource.StatusUnknown, err
}
glog.V(7).Infof("resource[%v].Create(urn=%v,...) success: id=%v; #outs=%v", p.pkg, urn, id, len(outs))
return id, outs, resource.StatusOK, nil
}
// Update updates an existing resource with new values.
func (p *provider) Update(urn resource.URN, id resource.ID,
olds resource.PropertyMap, news resource.PropertyMap) (resource.PropertyMap, resource.Status, error) {
contract.Assert(urn != "")
contract.Assert(id != "")
contract.Assert(news != nil)
contract.Assert(olds != nil)
glog.V(7).Infof("resource[%v].Update(id=%v,urn=%v,#olds=%v,#news=%v) executing",
p.pkg, id, urn, len(olds), len(news))
molds, err := MarshalProperties(olds, MarshalOptions{ElideAssetContents: true})
if err != nil {
return nil, resource.StatusOK, err
}
mnews, err := MarshalProperties(news, MarshalOptions{})
if err != nil {
return nil, resource.StatusOK, err
}
req := &pulumirpc.UpdateRequest{
Id: string(id),
Urn: string(urn),
Olds: molds,
News: mnews,
}
resp, err := p.client.Update(p.ctx.Request(), req)
if err != nil {
glog.V(7).Infof("resource[%v].Update(id=%v,urn=%v,...) failed: %v", p.pkg, id, urn, err)
return nil, resource.StatusUnknown, err
}
outs, err := UnmarshalProperties(resp.GetProperties(), MarshalOptions{})
if err != nil {
return nil, resource.StatusUnknown, err
}
glog.V(7).Infof("resource[%v].Update(id=%v,urn=%v,...) success; #out=%v", p.pkg, id, urn, len(outs))
return outs, resource.StatusOK, nil
}
// Delete tears down an existing resource.
func (p *provider) Delete(urn resource.URN, id resource.ID, props resource.PropertyMap) (resource.Status, error) {
contract.Assert(urn != "")
contract.Assert(id != "")
mprops, err := MarshalProperties(props, MarshalOptions{ElideAssetContents: true})
if err != nil {
return resource.StatusOK, err
}
glog.V(7).Infof("resource[%v].Delete(id=%v,urn=%v) executing", p.pkg, id, urn)
req := &pulumirpc.DeleteRequest{
Id: string(id),
Urn: string(urn),
Properties: mprops,
}
if _, err := p.client.Delete(p.ctx.Request(), req); err != nil {
glog.V(7).Infof("resource[%v].Delete(id=%v,urn=%v) failed: %v", p.pkg, id, urn, err)
return resource.StatusUnknown, err
}
glog.V(7).Infof("resource[%v].Delete(id=%v,urn=%v) success", p.pkg, id, urn)
return resource.StatusOK, nil
}
// Invoke dynamically executes a built-in function in the provider.
func (p *provider) Invoke(tok tokens.ModuleMember, args resource.PropertyMap) (resource.PropertyMap,
[]CheckFailure, error) {
contract.Assert(tok != "")
margs, err := MarshalProperties(args, MarshalOptions{KeepUnknowns: true})
if err != nil {
return nil, nil, err
}
glog.V(7).Infof("resource[%v].Invoke(tok=%v,#args=%v) executing", tok, len(args))
resp, err := p.client.Invoke(p.ctx.Request(), &pulumirpc.InvokeRequest{Tok: string(tok), Args: margs})
if err != nil {
glog.V(7).Infof("resource[%v].Invoke(tok=%v,#args=%v) failed: %v", p.pkg, tok, len(args), err)
return nil, nil, err
}
// Unmarshal any return values.
ret, err := UnmarshalProperties(resp.GetReturn(), MarshalOptions{KeepUnknowns: true})
if err != nil {
return nil, nil, err
}
// And now any properties that failed verification.
var failures []CheckFailure
for _, failure := range resp.GetFailures() {
failures = append(failures, CheckFailure{resource.PropertyKey(failure.Property), failure.Reason})
}
glog.V(7).Infof("resource[%v].Invoke(tok=%v,#args=%v,#ret=%v,#failures=%v) success",
p.pkg, tok, len(args), len(ret), len(failures))
return ret, failures, nil
}
// GetPluginInfo returns this plugin's information.
func (p *provider) GetPluginInfo() (Info, error) {
glog.V(7).Infof("resource[%v].GetPluginInfo() executing", p.pkg)
resp, err := p.client.GetPluginInfo(p.ctx.Request(), &pbempty.Empty{})
if err != nil {
glog.V(7).Infof("resource[%v].GetPluginInfo() failed: err=%v", p.pkg, err)
return Info{}, err
}
return Info{
Name: p.plug.Bin,
Type: ResourceType,
Version: resp.Version,
}, nil
}
// Close tears down the underlying plugin RPC connection and process.
func (p *provider) Close() error {
return p.plug.Close()
}