Skip to content

Commit

Permalink
Expose fn::method function for resource method calls
Browse files Browse the repository at this point in the history
  • Loading branch information
aq17 committed Jan 20, 2023
1 parent 123779a commit 9e35b07
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 11 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
provider `docker:Image` resource.
[#423](https://github.com/pulumi/pulumi-yaml/pull/423)

- Introduce `fn::method` function for resource methods.
[#431](https://github.com/pulumi/pulumi-yaml/pull/431)

### Bug Fixes

- Avoid panicing for non-string map keys
Expand Down
82 changes: 72 additions & 10 deletions pkg/pulumiyaml/analyser.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,13 @@ func (tc *typeCache) TypeExpr(expr ast.Expr) schema.Type {
}

type typeCache struct {
resources map[*ast.ResourceDecl]schema.Type
configuration map[string]schema.Type
outputs map[string]schema.Type
exprs map[ast.Expr]schema.Type
resourceNames map[string]*ast.ResourceDecl
variableNames map[string]ast.Expr
resources map[*ast.ResourceDecl]schema.Type
configuration map[string]schema.Type
outputs map[string]schema.Type
exprs map[ast.Expr]schema.Type
resourceNames map[string]*ast.ResourceDecl
resourceMethods map[string][]*schema.Method
variableNames map[string]ast.Expr
}

func (tc *typeCache) registerResource(name string, resource *ast.ResourceDecl, typ schema.Type) {
Expand Down Expand Up @@ -574,6 +575,8 @@ func (tc *typeCache) typeResource(r *Runner, node resourceNode) bool {
return true
}
hint := pkg.ResourceTypeHint(typ)
tc.resourceMethods[node.Key.Value] = hint.Resource.Methods

var allProperties []string
for _, prop := range hint.Resource.InputProperties {
allProperties = append(allProperties, prop.Name)
Expand Down Expand Up @@ -797,7 +800,7 @@ func (tc *typeCache) typeInvoke(ctx *evalContext, t *ast.InvokeExpr) bool {
if o := hint.Outputs; o != nil {
for _, output := range o.Properties {
fields = append(fields, output.Name)
if strings.ToLower(t.Return.Value) == strings.ToLower(output.Name) {
if strings.EqualFold(t.Return.Value, output.Name) {
returnType = output.Type
validReturn = true
}
Expand All @@ -821,6 +824,62 @@ func (tc *typeCache) typeInvoke(ctx *evalContext, t *ast.InvokeExpr) bool {
return true
}

func (tc *typeCache) typeMethodCall(ctx *evalContext, t *ast.MethodExpr) bool {
res := tc.resourceNames[t.ResourceName.Property.String()]
version, err := ParseVersion(res.Options.Version)
if err != nil {
ctx.error(res.Options.Version, fmt.Sprintf("unable to parse resource provider version: %v", err))
return true
}
// Attempt to resolve the method if a full token is given, i.e. google-native:container/v1:Cluster/getKubeconfig
pkg, functionName, err := ResolveFunction(ctx.pkgLoader, t.FuncToken.Value, version)
if err != nil {
// If invalid, look up resource's methods to derive the token from the name, i.e. getKubeconfig
matched := false
if methods, ok := tc.resourceMethods[t.ResourceName.Property.String()]; ok {
for _, m := range methods {
if m.Name == t.FuncToken.Value {
pkg, functionName, err = ResolveFunction(ctx.pkgLoader, m.Function.Token, version)
t.FuncToken.Value = functionName.String()
matched = true
break
}
}
}
if !matched && err != nil {
_, b := ctx.error(t, err.Error())
return b
}
}
var existing []string
hint := pkg.FunctionTypeHint(functionName)
inputs := map[string]schema.Type{}
if hint.Inputs != nil {
for _, input := range hint.Inputs.Properties {
existing = append(existing, input.Name)
inputs[input.Name] = input.Type
}
}
fmtr := yamldiags.NonExistantFieldFormatter{
ParentLabel: fmt.Sprintf("Invoke %s", functionName.String()),
Fields: existing,
MaxElements: 5,
}
if t.CallArgs != nil {
for _, prop := range t.CallArgs.Entries {
k := prop.Key.(*ast.StringExpr).Value
if typ, ok := inputs[k]; !ok {
summary, detail := fmtr.MessageWithDetail(k, k)
subject := prop.Key.Syntax().Syntax().Range()
ctx.addWarnDiag(subject, summary, detail)
} else {
tc.exprs[prop.Value] = typ
}
}
}
return true
}

func (tc *typeCache) typeSymbol(ctx *evalContext, t *ast.SymbolExpr) bool {
var typ schema.Type = &schema.InvalidType{}
if root, ok := tc.resourceNames[t.Property.RootName()]; ok {
Expand Down Expand Up @@ -962,6 +1021,8 @@ func (tc *typeCache) typeExpr(ctx *evalContext, t ast.Expr) bool {
switch t := t.(type) {
case *ast.InvokeExpr:
return tc.typeInvoke(ctx, t)
case *ast.MethodExpr:
return tc.typeMethodCall(ctx, t)
case *ast.SymbolExpr:
return tc.typeSymbol(ctx, t)
case *ast.StringExpr:
Expand Down Expand Up @@ -1158,9 +1219,10 @@ func newTypeCache() *typeCache {
},
},
},
resources: map[*ast.ResourceDecl]schema.Type{},
configuration: map[string]schema.Type{},
resourceNames: map[string]*ast.ResourceDecl{},
resources: map[*ast.ResourceDecl]schema.Type{},
configuration: map[string]schema.Type{},
resourceNames: map[string]*ast.ResourceDecl{},
resourceMethods: map[string][]*schema.Method{},
variableNames: map[string]ast.Expr{
PulumiVarName: pulumiExpr,
},
Expand Down
76 changes: 76 additions & 0 deletions pkg/pulumiyaml/ast/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,24 @@ func Invoke(token string, callArgs *ObjectExpr, callOpts InvokeOptionsDecl, ret
}
}

// MethodExpr is a function expression that invokes a Pulumi resource method by type token + resource.
type MethodExpr struct {
builtinNode

FuncToken *StringExpr
ResourceName *SymbolExpr
CallArgs *ObjectExpr
}

func MethodSyntax(node *syntax.ObjectNode, name *StringExpr, args *ObjectExpr, token *StringExpr, resourceName *SymbolExpr, callArgs *ObjectExpr) *MethodExpr {
return &MethodExpr{
builtinNode: builtin(node, name, args),
FuncToken: token,
ResourceName: resourceName,
CallArgs: callArgs,
}
}

// ToJSON returns the underlying structure as a json string.
type ToJSONExpr struct {
builtinNode
Expand Down Expand Up @@ -760,6 +778,8 @@ func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool)
switch strings.ToLower(kvp.Key.Value()) {
case "fn::invoke":
set("fn::invoke", parseInvoke)
case "fn::method":
set("fn::method", parseMethod)
case "fn::join":
set("fn::join", parseJoin)
case "fn::tojson":
Expand Down Expand Up @@ -898,6 +918,62 @@ func parseInvoke(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, sy
return InvokeSyntax(node, name, obj, function, arguments, opts, ret), diags
}

func parseMethod(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
obj, ok := args.(*ObjectExpr)
if !ok {
return nil, syntax.Diagnostics{ExprError(args, "the argument to fn::method must be an object containing 'resource', 'method', and 'arguments'", "")}
}
var methodExpr, selfExpr, argumentsExpr Expr
var diags syntax.Diagnostics

for i := 0; i < len(obj.Entries); i++ {
kvp := obj.Entries[i]
if str, ok := kvp.Key.(*StringExpr); ok {
switch strings.ToLower(str.Value) {
case "method":
diags.Extend(syntax.UnexpectedCasing(str.syntax.Syntax().Range(), "method", str.GetValue()))
methodExpr = kvp.Value
case "resource":
diags.Extend(syntax.UnexpectedCasing(str.syntax.Syntax().Range(), "resource", str.GetValue()))
selfExpr = kvp.Value
case "arguments":
diags.Extend(syntax.UnexpectedCasing(str.syntax.Syntax().Range(), "arguments", str.GetValue()))
argumentsExpr = kvp.Value
}
}
}

method, ok := methodExpr.(*StringExpr)
if !ok {
if methodExpr == nil {
diags.Extend(ExprError(obj, "missing method name ('method')", ""))
} else {
diags.Extend(ExprError(methodExpr, "method name must be a string literal", ""))
}
}

self, ok := selfExpr.(*SymbolExpr)
if !ok {
if selfExpr == nil {
diags.Extend(ExprError(obj, "missing resource name ('resource')", ""))
} else {
diags.Extend(ExprError(selfExpr, "resource key must reference a declared resource", ""))
}
}

arguments, ok := argumentsExpr.(*ObjectExpr)
if !ok && argumentsExpr != nil {
diags.Extend(ExprError(argumentsExpr, "function arguments ('arguments') must be an object", ""))
}

if diags.HasErrors() {
return nil, diags
}

return MethodSyntax(node, name, obj, method, self, arguments), diags

}

func parseJoin(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) {
list, ok := args.(*ListExpr)
if !ok || len(list.Elements) != 2 {
Expand Down
32 changes: 31 additions & 1 deletion pkg/pulumiyaml/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,6 @@ func (e *programEvaluator) registerResource(kvp resourceNode) (lateboundResource
}
isComponent = result
}

constants := pkg.ResourceConstants(typ)
for k, v := range constants {
props[k] = v
Expand Down Expand Up @@ -1359,6 +1358,8 @@ func (e *programEvaluator) evaluateExpr(x ast.Expr) (interface{}, bool) {
return e.evaluatePropertyAccess(x, x.Property)
case *ast.InvokeExpr:
return e.evaluateBuiltinInvoke(x)
case *ast.MethodExpr:
return e.evaluateBuiltinMethodCall(x)
case *ast.JoinExpr:
return e.evaluateBuiltinJoin(x)
case *ast.SplitExpr:
Expand Down Expand Up @@ -1758,6 +1759,35 @@ func (e *programEvaluator) evaluateBuiltinInvoke(t *ast.InvokeExpr) (interface{}
return performInvoke(args)
}

func (e *programEvaluator) evaluateBuiltinMethodCall(t *ast.MethodExpr) (interface{}, bool) {
args, ok := e.evaluateExpr(t.CallArgs)
if !ok {
return nil, false
}

performCall := e.lift(func(args ...interface{}) (interface{}, bool) {
r, ok := e.evaluateExpr(t.ResourceName)
if !ok {
e.error(t.ResourceName, fmt.Sprintf("unable to resolve resource name: %v", t.ResourceName))
return nil, false
}
res := r.(lateboundResource)

_, functionName, err := ResolveFunction(e.pkgLoader, t.FuncToken.Value, nil)
if err != nil {
return e.error(t, err.Error())
}

if output, err := e.pulumiCtx.Call(string(functionName), pulumi.ToMap(args[0].(map[string]interface{})),
pulumi.AnyOutput{}, res.CustomResource()); err != nil {
return e.error(t, err.Error())
} else {
return output, true
}
})
return performCall(args)
}

func (e *programEvaluator) evaluateBuiltinJoin(v *ast.JoinExpr) (interface{}, bool) {
overallOk := true

Expand Down
31 changes: 31 additions & 0 deletions pkg/pulumiyaml/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var packageReadmeFile string

const testComponentToken = "test:component:type"
const testResourceToken = "test:resource:type"
const testResourceWithMethodsToken = "test:resourcewithmethods:type"

type MockPackageLoader struct {
packages map[string]Package
Expand Down Expand Up @@ -101,6 +102,22 @@ func inputProperties(token string, props ...schema.Property) *schema.ResourceTyp
}
}

func inputPropertiesWithMethods(token string, methods []*schema.Method, props ...schema.Property) *schema.ResourceType {
p := make([]*schema.Property, 0, len(props))
for _, prop := range props {
prop := prop
p = append(p, &prop)
}
return &schema.ResourceType{
Resource: &schema.Resource{
Token: token,
InputProperties: p,
Properties: p,
Methods: methods,
},
}
}

func function(token string, inputs, outputs []schema.Property) *schema.Function {
pIn := make([]*schema.Property, 0, len(inputs))
pOut := make([]*schema.Property, 0, len(outputs))
Expand Down Expand Up @@ -149,6 +166,20 @@ func newMockPackageMap() PackageLoader {
Name: "foo",
Type: schema.StringType,
})
case testResourceWithMethodsToken:
return inputPropertiesWithMethods(typeName, []*schema.Method{
{
Name: "fooFunc",
Function: &schema.Function{
Token: "test:method:type",
ReturnType: schema.BoolType,
},
},
},
schema.Property{
Name: "foo",
Type: schema.StringType,
})
default:
return inputProperties(typeName)
}
Expand Down

0 comments on commit 9e35b07

Please sign in to comment.