-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Functions/datasources should accept Outputs #5758
Comments
Semi-related to #5385. For multi-lang components, we want to be able to support functions referencing resources. To do so, if a function takes a resource as an argument, we'll update codegen to emit the function as accepting |
Related to both comments above, I was playing with an idea of yet another form const a = new ResourceA("name");
const b = a.someFunction(); This is based on Azure NextGen where invokes ~always "belong" to a parent resource and the relationship is easy to derive. WDYT? |
We are planning to add support for "resource methods" as part of the multi-lang component work. I send you a link to the design doc. |
I'm not sure that I agree that this is an improvement, especially for the first-time experience. How do we describe when to use which? Do we update examples to use |
~all documentation would point at the
Yes - almost all examples, and all code-generated examples, would use the
Having tried to explain this hundreds of times, at this point I believe we have to make a product change. Open to suggestions on alternative solutions, but this is just too significant of a usability issue to leave to just "explaining". |
Do you believe that it's most common to call functions using outputs from resources and then use them as pure inputs to other resources (as opposed to using the results for control flow)? |
FWIW, this is true for my experience with Azure NextGen and beyond. |
As a concrete example - this is the code we will be adding to our azure Python template: # Export the primary key of the Storage Account
primary_key = pulumi.Output.all(resource_group.name, account.name) \
.apply(lambda args: storage.list_storage_account_keys(
resource_group_name=args[0],
account_name=args[1]
)).apply(lambda accountKeys: accountKeys.keys[0].value) If we had an Output form of the function - that whole thing would be: # Export the primary key of the Storage Account
primary_key =
storage.list_storage_account_keys_output(resource_group_name=resource_group.name, account_name=account.name).keys[0].value |
Tangentially related, this style of composition is what Haskellers eat for breakfast under the name of Applicative functors. pure :: a -> f a
( <*> ) :: f (a -> b) -> f a -> f b
liftA2 f x y = f <$> x <*> y
pure storage.list_storage_account_keys <*> resource_group.name <*> account.name ==
storage.list_storage_account_keys <$> resource_group.name <*> account.name ==
liftA2 storage.list_storage_account_keys resource_group.name account.name This version of lift_a2 checks under mypy: from pulumi import Output
from typing import Optional, Callable, TypeVar, List, Any
import pulumi
class InvokeOptions:
pass
class Key:
value: str
class ListStorageAccountKeysResult:
keys: List[Key]
def list_storage_account_keys(account_name: Optional[str] = None,
expand: Optional[str] = None,
resource_group_name: Optional[str] = None,
opts: Optional[InvokeOptions] = None) -> ListStorageAccountKeysResult:
...
T1 = TypeVar('T1')
T2 = TypeVar('T2')
T3 = TypeVar('T3')
def lift_a2(f: Callable[[T1, T2], T3]) -> Callable[[Output[T1], Output[T2]], Output[T3]]:
def f_any(xy: Any) -> T3:
return f(xy[0], xy[1])
return lambda x, y: pulumi.Output.all([x, y]).apply(f_any)
def example(account_name: Output[Optional[str]], resource_group_name: Output[Optional[str]]) -> Output[str]:
return lift_a2(list_storage_account_keys)(account_name, resource_group_name) \
.apply(lambda x: x.keys[0].value) |
There is also a code style for this more suitable to imperative languages, I think I first saw it in Knockout JS library. The idea is that there is a magic "Output builder" function that accepts a pure (this is crucial) lambda that is allowed to evaluate any
The
For monadic composition we need a variation that lets the body return a nested Output[T] and unwraps it later:
Purity of the builder lambda is important because the implementation needs to call the lambda at least twice, once with junk arguments, to introspect its body. The constraint is more than purity, in fact, it is assumed that the lambda does not introspect and branch on the arguments. This makes it slightly controversial. The notation is certainly super convenient but these restrictions may fly in the way of the programmer intuition. Again Knockout JS lib got away with this. |
Chatting with @pgavlin (thanks a ton), concrete steps for me here are:
|
BTW - a sketch of what I expected this to look like for TypeScript is in 54dad85. |
Quick update, I'm sorry for some delay getting all the subtasks done here. After chatting with @emiliza I've broken down the remaining work so it's easier to see in our planning so we see where we stand on this. Go and Python are done. Remainder: C# - PR done with tests but waiting on providers to up the SDK Node Update conceptual docs Update docs gen so that this shows up in API reference documentation Update examples to use this style Update program gen so that generated examples use this style Looks like I've already opened a set of similar issues before, let me clean this up as dups right now. @ericrudder the list above may help answer your questions on the "big picture view" here, if not please let me know if you have further questions. |
One of the most common usability issues I've seen new users hit with Pulumi is that datasources/functions take and return raw values instead of Inputs/Outputs, meaning they are very difficult to compose with the rest of the Pulumi resource model. We encourage folks to get used to writing code like this in the normal Pulumi usage:
But then when you want to do the same with a Pulumi function - it doesn't work.
Instead - you have to write something like this:
This is not obvious, and not pleasant.
The original motivation was to provide the additional flexibility of being able to use these functions in cases where you don't need to get caught up in
Outputs
. And it may be we feel we have to continue to support those use cases.A non-breaking way to improve this would be to project two copies of every function
somFunc
andsomeFuncOutput
for example - where the latter accepts Inputs and returns Outputs.Engineering Work Items
M64
M63
PR: 5758 for C#/.NET #7899
M62
PR: 5758 for Node JS #8047
M61
Future Iterations
The text was updated successfully, but these errors were encountered: