pulumi/pkg/resource/deploy/builtins.go

393 lines
11 KiB
Go

package deploy
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
uuid "github.com/gofrs/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
)
type builtinProvider struct {
context context.Context
cancel context.CancelFunc
backendClient BackendClient
resources *resourceMap
callbackClients *callbackMap
}
func newBuiltinProvider(backendClient BackendClient, resources *resourceMap) *builtinProvider {
ctx, cancel := context.WithCancel(context.Background())
return &builtinProvider{
context: ctx,
cancel: cancel,
backendClient: backendClient,
resources: resources,
callbackClients: &callbackMap{},
}
}
func (p *builtinProvider) Close() error {
return nil
}
func (p *builtinProvider) Pkg() tokens.Package {
return "pulumi"
}
// GetSchema returns the JSON-serialized schema for the provider.
func (p *builtinProvider) GetSchema(version int) ([]byte, error) {
return []byte("{}"), nil
}
func (p *builtinProvider) GetMapping(key string) ([]byte, string, error) {
return nil, "", nil
}
// CheckConfig validates the configuration for this resource provider.
func (p *builtinProvider) CheckConfig(urn resource.URN, olds,
news resource.PropertyMap, allowUnknowns bool) (resource.PropertyMap, []plugin.CheckFailure, error) {
return nil, nil, nil
}
// DiffConfig checks what impacts a hypothetical change to this provider's configuration will have on the provider.
func (p *builtinProvider) DiffConfig(urn resource.URN, olds, news resource.PropertyMap,
allowUnknowns bool, ignoreChanges []string) (plugin.DiffResult, error) {
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
func (p *builtinProvider) Configure(props resource.PropertyMap) error {
return nil
}
const stackReferenceType = "pulumi:pulumi:StackReference"
func (p *builtinProvider) Check(urn resource.URN, state, inputs resource.PropertyMap,
allowUnknowns bool, randomSeed []byte) (resource.PropertyMap, []plugin.CheckFailure, error) {
typ := urn.Type()
if typ != stackReferenceType {
return nil, nil, fmt.Errorf("unrecognized resource type '%v'", urn.Type())
}
var name resource.PropertyValue
for k := range inputs {
if k != "name" {
return nil, []plugin.CheckFailure{{Property: k, Reason: fmt.Sprintf("unknown property \"%v\"", k)}}, nil
}
}
name, ok := inputs["name"]
if !ok {
return nil, []plugin.CheckFailure{{Property: "name", Reason: `missing required property "name"`}}, nil
}
if !name.IsString() && !name.IsComputed() {
return nil, []plugin.CheckFailure{{Property: "name", Reason: `property "name" must be a string`}}, nil
}
return inputs, nil, nil
}
func (p *builtinProvider) Diff(urn resource.URN, id resource.ID, state, inputs resource.PropertyMap,
allowUnknowns bool, ignoreChanges []string) (plugin.DiffResult, error) {
contract.Assert(urn.Type() == stackReferenceType)
if !inputs["name"].DeepEquals(state["name"]) {
return plugin.DiffResult{
Changes: plugin.DiffSome,
ReplaceKeys: []resource.PropertyKey{"name"},
}, nil
}
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
func (p *builtinProvider) Create(urn resource.URN, inputs resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
contract.Assert(urn.Type() == stackReferenceType)
state, err := p.readStackReference(inputs)
if err != nil {
return "", nil, resource.StatusUnknown, err
}
var id resource.ID
if !preview {
// generate a new uuid
uuid, err := uuid.NewV4()
if err != nil {
return "", nil, resource.StatusOK, err
}
id = resource.ID(uuid.String())
}
return id, state, resource.StatusOK, nil
}
func (p *builtinProvider) Update(urn resource.URN, id resource.ID, state, inputs resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
contract.Failf("unexpected update for builtin resource %v", urn)
contract.Assert(urn.Type() == stackReferenceType)
return state, resource.StatusOK, errors.New("unexpected update for builtin resource")
}
func (p *builtinProvider) Delete(urn resource.URN, id resource.ID,
state resource.PropertyMap, timeout float64) (resource.Status, error) {
contract.Assert(urn.Type() == stackReferenceType)
return resource.StatusOK, nil
}
func (p *builtinProvider) Read(urn resource.URN, id resource.ID,
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
contract.Assertf(urn != "", "Read URN was empty")
contract.Assertf(id != "", "Read ID was empty")
contract.Assert(urn.Type() == stackReferenceType)
outputs, err := p.readStackReference(state)
if err != nil {
return plugin.ReadResult{}, resource.StatusUnknown, err
}
return plugin.ReadResult{
Inputs: inputs,
Outputs: outputs,
}, resource.StatusOK, nil
}
func (p *builtinProvider) Construct(info plugin.ConstructInfo, typ tokens.Type, name tokens.QName, parent resource.URN,
inputs resource.PropertyMap, options plugin.ConstructOptions) (plugin.ConstructResult, error) {
return plugin.ConstructResult{}, errors.New("builtin resources may not be constructed")
}
const readStackOutputs = "pulumi:pulumi:readStackOutputs"
const readStackResourceOutputs = "pulumi:pulumi:readStackResourceOutputs"
const getResource = "pulumi:pulumi:getResource"
const invokeCallback = "pulumi:pulumi:invokeCallback"
func (p *builtinProvider) Invoke(tok tokens.ModuleMember,
args resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
switch tok {
case readStackOutputs:
outs, err := p.readStackReference(args)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
case readStackResourceOutputs:
outs, err := p.readStackResourceOutputs(args)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
case getResource:
outs, err := p.getResource(args)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
case invokeCallback:
outs, err := p.invokeCallback(args)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
default:
return nil, nil, fmt.Errorf("unrecognized function name: '%v'", tok)
}
}
func (p *builtinProvider) StreamInvoke(
tok tokens.ModuleMember, args resource.PropertyMap,
onNext func(resource.PropertyMap) error) ([]plugin.CheckFailure, error) {
return nil, fmt.Errorf("the builtin provider does not implement streaming invokes")
}
func (p *builtinProvider) Call(tok tokens.ModuleMember, args resource.PropertyMap, info plugin.CallInfo,
options plugin.CallOptions) (plugin.CallResult, error) {
return plugin.CallResult{}, fmt.Errorf("the builtin provider does not implement call")
}
func (p *builtinProvider) GetPluginInfo() (workspace.PluginInfo, error) {
// return an error: this should not be called for the builtin provider
return workspace.PluginInfo{}, errors.New("the builtin provider does not report plugin info")
}
func (p *builtinProvider) SignalCancellation() error {
p.cancel()
return nil
}
func (p *builtinProvider) readStackReference(inputs resource.PropertyMap) (resource.PropertyMap, error) {
name, ok := inputs["name"]
contract.Assert(ok)
contract.Assert(name.IsString())
if p.backendClient == nil {
return nil, errors.New("no backend client is available")
}
outputs, err := p.backendClient.GetStackOutputs(p.context, name.StringValue())
if err != nil {
return nil, err
}
secretOutputs := make([]resource.PropertyValue, 0)
for k, v := range outputs {
if v.ContainsSecrets() {
secretOutputs = append(secretOutputs, resource.NewStringProperty(string(k)))
}
}
// Sort the secret outputs so the order is deterministic, to avoid spurious diffs during updates.
sort.Slice(secretOutputs, func(i, j int) bool {
return secretOutputs[i].String() < secretOutputs[j].String()
})
return resource.PropertyMap{
"name": name,
"outputs": resource.NewObjectProperty(outputs),
"secretOutputNames": resource.NewArrayProperty(secretOutputs),
}, nil
}
func (p *builtinProvider) readStackResourceOutputs(inputs resource.PropertyMap) (resource.PropertyMap, error) {
name, ok := inputs["stackName"]
contract.Assert(ok)
contract.Assert(name.IsString())
if p.backendClient == nil {
return nil, errors.New("no backend client is available")
}
outputs, err := p.backendClient.GetStackResourceOutputs(p.context, name.StringValue())
if err != nil {
return nil, err
}
return resource.PropertyMap{
"name": name,
"outputs": resource.NewObjectProperty(outputs),
}, nil
}
func (p *builtinProvider) getResource(inputs resource.PropertyMap) (resource.PropertyMap, error) {
urn, ok := inputs["urn"]
contract.Assert(ok)
contract.Assert(urn.IsString())
state, ok := p.resources.get(resource.URN(urn.StringValue()))
if !ok {
return nil, fmt.Errorf("unknown resource %v", urn.StringValue())
}
return resource.PropertyMap{
"urn": urn,
"id": resource.NewStringProperty(string(state.ID)),
"state": resource.NewObjectProperty(state.Outputs),
}, nil
}
func (p *builtinProvider) invokeCallback(inputs resource.PropertyMap) (resource.PropertyMap, error) {
referenceProp, ok := inputs["reference"]
contract.Assert(ok)
contract.Assert(referenceProp.IsString())
reference := referenceProp.StringValue()
parts := strings.Split(reference, "/")
if len(parts) == 0 {
return nil, fmt.Errorf("invalid callback reference %s", reference)
}
address := parts[0]
if address == "" {
return nil, fmt.Errorf("no address in callback reference %s", reference)
}
client, ok := p.callbackClients.get(address)
if !ok {
target := fmt.Sprintf("127.0.0.1:%s", address)
conn, err := grpc.Dial(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("did not connect: %w", err)
}
client = pulumirpc.NewCallbackClient(conn)
p.callbackClients.set(address, client)
}
argsProp, ok := inputs["args"]
contract.Assert(ok)
contract.Assert(argsProp.IsObject())
args := argsProp.ObjectValue()
argsStruct, err := plugin.MarshalProperties(args, plugin.MarshalOptions{
KeepUnknowns: true,
KeepSecrets: true,
KeepResources: true,
KeepOutputValues: true,
})
if err != nil {
return nil, fmt.Errorf("marshaling args: %w", err)
}
// TODO plumb through the context?
resp, err := client.Invoke(context.Background(), &pulumirpc.CallbackInvokeRequest{
Reference: reference,
Args: argsStruct,
})
if err != nil {
return nil, fmt.Errorf("invoking the callback: %w", err)
}
result, err := plugin.UnmarshalProperties(resp.Return, plugin.MarshalOptions{
KeepUnknowns: true,
KeepSecrets: true,
KeepResources: true,
KeepOutputValues: true,
})
if err != nil {
return nil, fmt.Errorf("unmarshaling return: %w", err)
}
return result, nil
}
type callbackMap struct {
m sync.Map
}
func (m *callbackMap) set(address string, client pulumirpc.CallbackClient) {
m.m.Store(address, client)
}
func (m *callbackMap) get(address string) (pulumirpc.CallbackClient, bool) {
if m == nil {
panic(fmt.Errorf("justin omg nil"))
}
c, ok := m.m.Load(address)
if !ok {
return nil, false
}
return c.(pulumirpc.CallbackClient), true
}