pulumi/sdk/go/pulumi/provider.go

303 lines
9 KiB
Go

// Copyright 2016-2021, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pulumi
import (
"context"
"reflect"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"google.golang.org/grpc"
)
type constructFunc func(ctx *Context, typ, name string, inputs map[string]interface{},
options ResourceOption) (URNInput, Input, error)
// construct adapts the gRPC ConstructRequest/ConstructResponse to/from the Pulumi Go SDK programming model.
func construct(ctx context.Context, req *pulumirpc.ConstructRequest, engineConn *grpc.ClientConn,
constructF constructFunc) (*pulumirpc.ConstructResponse, error) {
// Configure the RunInfo.
runInfo := RunInfo{
Project: req.GetProject(),
Stack: req.GetStack(),
Config: req.GetConfig(),
ConfigSecretKeys: req.GetConfigSecretKeys(),
Parallel: int(req.GetParallel()),
DryRun: req.GetDryRun(),
MonitorAddr: req.GetMonitorEndpoint(),
engineConn: engineConn,
}
pulumiCtx, err := NewContext(ctx, runInfo)
if err != nil {
return nil, errors.Wrap(err, "constructing run context")
}
// Deserialize the inputs and apply appropriate dependencies.
inputDependencies := req.GetInputDependencies()
deserializedInputs, err := plugin.UnmarshalProperties(
req.GetInputs(),
plugin.MarshalOptions{KeepSecrets: true, KeepResources: true, KeepUnknowns: req.GetDryRun()},
)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling inputs")
}
inputs := make(map[string]interface{}, len(deserializedInputs))
for key, value := range deserializedInputs {
k := string(key)
var deps []Resource
if inputDeps, ok := inputDependencies[k]; ok {
deps = make([]Resource, len(inputDeps.GetUrns()))
for i, depURN := range inputDeps.GetUrns() {
deps[i] = pulumiCtx.newDependencyResource(URN(depURN))
}
}
inputs[k] = &constructInput{
value: value,
deps: deps,
}
}
// Rebuild the resource options.
aliases := make([]Alias, len(req.GetAliases()))
for i, urn := range req.GetAliases() {
aliases[i] = Alias{URN: URN(urn)}
}
dependencies := make([]Resource, len(req.GetDependencies()))
for i, urn := range req.GetDependencies() {
dependencies[i] = pulumiCtx.newDependencyResource(URN(urn))
}
providers := make(map[string]ProviderResource, len(req.GetProviders()))
for pkg, ref := range req.GetProviders() {
// Parse the URN and ID out of the provider reference.
lastSep := strings.LastIndex(ref, "::")
if lastSep == -1 {
return nil, errors.Errorf("expected '::' in provider reference %s", ref)
}
urn := ref[0:lastSep]
id := ref[lastSep+2:]
providers[pkg] = pulumiCtx.newDependencyProviderResource(URN(urn), ID(id))
}
var parent Resource
if req.GetParent() != "" {
parent = pulumiCtx.newDependencyResource(URN(req.GetParent()))
}
opts := resourceOption(func(ro *resourceOptions) {
ro.Aliases = aliases
ro.DependsOn = dependencies
ro.Protect = req.GetProtect()
ro.Providers = providers
ro.Parent = parent
})
urn, state, err := constructF(pulumiCtx, req.GetType(), req.GetName(), inputs, opts)
if err != nil {
return nil, err
}
// Wait for async work to finish.
if err = pulumiCtx.wait(); err != nil {
return nil, err
}
rpcURN, _, _, err := urn.ToURNOutput().awaitURN(ctx)
if err != nil {
return nil, err
}
// Serialize all state properties, first by awaiting them, and then marshaling them to the requisite gRPC values.
resolvedProps, propertyDeps, _, err := marshalInputs(state)
if err != nil {
return nil, errors.Wrap(err, "marshaling properties")
}
// Marshal all properties for the RPC call.
keepUnknowns := req.GetDryRun()
rpcProps, err := plugin.MarshalProperties(
resolvedProps,
plugin.MarshalOptions{KeepSecrets: true, KeepUnknowns: keepUnknowns, KeepResources: pulumiCtx.keepResources})
if err != nil {
return nil, errors.Wrap(err, "marshaling properties")
}
// Convert the property dependencies map for RPC and remove duplicates.
rpcPropertyDeps := make(map[string]*pulumirpc.ConstructResponse_PropertyDependencies)
for k, deps := range propertyDeps {
sort.Slice(deps, func(i, j int) bool { return deps[i] < deps[j] })
urns := make([]string, 0, len(deps))
for i, d := range deps {
if i > 0 && urns[i-1] == string(d) {
continue
}
urns = append(urns, string(d))
}
rpcPropertyDeps[k] = &pulumirpc.ConstructResponse_PropertyDependencies{
Urns: urns,
}
}
return &pulumirpc.ConstructResponse{
Urn: string(rpcURN),
State: rpcProps,
StateDependencies: rpcPropertyDeps,
}, nil
}
type constructInput struct {
value resource.PropertyValue
deps []Resource
}
// constructInputsMap returns the inputs as a Map.
func constructInputsMap(ctx *Context, inputs map[string]interface{}) (Map, error) {
result := make(Map, len(inputs))
for k, v := range inputs {
ci := v.(*constructInput)
known := !ci.value.ContainsUnknowns()
value, secret, err := unmarshalPropertyValue(ctx, ci.value)
if err != nil {
return nil, errors.Wrapf(err, "unmarshaling input %s", k)
}
resultType := anyOutputType
if ot, ok := concreteTypeToOutputType.Load(reflect.TypeOf(value)); ok {
resultType = ot.(reflect.Type)
}
output := ctx.newOutput(resultType, ci.deps...)
output.getState().resolve(value, known, secret, nil)
result[k] = output
}
return result, nil
}
// constructInputsCopyTo sets the inputs on the given args struct.
func constructInputsCopyTo(ctx *Context, inputs map[string]interface{}, args interface{}) error {
if args == nil {
return errors.New("args must not be nil")
}
argsV := reflect.ValueOf(args)
typ := argsV.Type()
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return errors.New("args must be a pointer to a struct")
}
argsV, typ = argsV.Elem(), typ.Elem()
for k, v := range inputs {
ci := v.(*constructInput)
for i := 0; i < typ.NumField(); i++ {
fieldV := argsV.Field(i)
if !fieldV.CanSet() {
continue
}
field := typ.Field(i)
tag, has := field.Tag.Lookup("pulumi")
if !has || tag != k {
continue
}
if field.Type.Implements(outputType) || field.Type.Implements(inputType) {
resultType := anyOutputType
if field.Type.Implements(outputType) {
resultType = field.Type
} else if field.Type.Implements(inputType) {
toOutputMethodName := "To" + strings.TrimSuffix(field.Type.Name(), "Input") + "Output"
if toOutputMethod, found := field.Type.MethodByName(toOutputMethodName); found {
mt := toOutputMethod.Type
if mt.NumIn() == 0 && mt.NumOut() == 1 && mt.Out(0).Implements(outputType) {
resultType = mt.Out(0)
}
}
}
output := ctx.newOutput(resultType, ci.deps...)
dest := reflect.New(output.ElementType()).Elem()
known := !ci.value.ContainsUnknowns()
secret, err := unmarshalOutput(ctx, ci.value, dest)
if err != nil {
return err
}
output.getState().resolve(dest.Interface(), known, secret, nil)
fieldV.Set(reflect.ValueOf(output))
continue
}
if len(ci.deps) > 0 {
return errors.Errorf(
"%s.%s is typed as %v but must be typed as Input or Output for input %q with dependencies",
typ, field.Name, field.Type, k)
}
dest := reflect.New(field.Type).Elem()
secret, err := unmarshalOutput(ctx, ci.value, dest)
if err != nil {
return errors.Wrapf(err, "unmarshaling input %s", k)
}
if secret {
return errors.Errorf(
"%s.%s is typed as %v but must be typed as Input or Output for secret input %q",
typ, field.Name, field.Type, k)
}
fieldV.Set(reflect.ValueOf(dest.Interface()))
}
}
return nil
}
// newConstructResult converts a resource into its associated URN and state.
func newConstructResult(resource ComponentResource) (URNInput, Input, error) {
if resource == nil {
return nil, nil, errors.New("resource must not be nil")
}
resourceV := reflect.ValueOf(resource)
typ := resourceV.Type()
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return nil, nil, errors.New("resource must be a pointer to a struct")
}
resourceV, typ = resourceV.Elem(), typ.Elem()
state := make(Map)
for i := 0; i < typ.NumField(); i++ {
fieldV := resourceV.Field(i)
if !fieldV.CanInterface() {
continue
}
field := typ.Field(i)
tag, has := field.Tag.Lookup("pulumi")
if !has {
continue
}
val := fieldV.Interface()
if v, ok := val.(Input); ok {
state[tag] = v
} else {
state[tag] = ToOutput(val)
}
}
return resource.URN(), state, nil
}