Functions Now Accept Outputs
Posted on
Pulumi 3.17.1 makes it easier to compose function calls and resources.
In practice you often need to call a function with a resource output.
Previous versions of Pulumi required an apply
to do this, which
was unfortunate:
new Pulumi users would get stuck and ask for help as the solution was not obvious
experienced users found the code unpleasant, upvoting the relevant GitHub Issue
With Pulumi 3.17.1 you can now call functions directly with resource
outputs without an extra apply
. Every function now has an additional
Output form that accepts Input
-typed arguments and returns an
Output
-wrapped result.
For a quick example, here is how you can call
aws.ecr.getCredentials
with a registryId
of type Output<string>
:
const registryId: Output<string> = ...
getCredentialsOutput({registryId: registryId}): Output<GetCredentialsResult>
registry_id: Output[str] = ...
get_credentials_output(registry_id=registryId): Output[GetCredentialsResult]
var registryId StringOutput
var result GetCredentialsResultOutput
result = GetCredentialsOutput(ctx, GetCredentialsOutputArgs{
RegistryId: result
})
Output<string> registryId;
GetCredentials.Invoke(new GetCredentialsInvokeArgs
{
RegistryId = registryId
});
Complete Example: Publish Docker Image to ECR
Why would you call aws.ecr.getCredentials
with an Output
? Suppose
you want to provision an AWS Elastic Container Registry (ECR)
repository, build a Docker image locally and publish this image to the
registry.
To configure the Docker Image resource, you need ECR credentials.
To acquire the credentials, you need to call the
aws.ecr.getCredentials
function with the ECR registry ID.Because the ECR registry ID is only known once the actual repository is provisioned in the cloud, the registryId property of the ecr.Repository resource has the type
Output<string>
rather than string (see Inputs and Outputs).
In the code below, note how getCredentialsOutput
now accepts
appRepo.registryId
directly:
import * as aws from "@pulumi/aws";
import * as docker from "@pulumi/docker";
function parseAuthToken(authToken: string): {username: string, password: string} {
const parts = Buffer.from(authToken, "base64").toString("ascii").split(":");
console.log(parts);
return {
username: parts[0],
password: parts[1]
}
}
const appRepo = new aws.ecr.Repository("app-repo");
const creds = aws.ecr.getCredentialsOutput({registryId: appRepo.registryId})
.apply(creds => parseAuthToken(creds.authorizationToken))
const image = new docker.Image("app-img", {
// ./my-app is a folder with a Dockerfile
build: "./my-app",
imageName: appRepo.repositoryUrl,
registry: {
server: appRepo.repositoryUrl,
username: creds.username,
password: creds.password
}
});
export const imageUrn = image.urn;
from collections import namedtuple
import base64
from pulumi_aws import s3, ecr
import pulumi_docker as docker
Creds = namedtuple('Creds', 'username password')
def parse_auth_token(token):
(u, p) = base64.b64decode(token).decode().split(':')
return Creds(username=u, password=p)
repo = ecr.Repository('app-repo')
creds = ecr.get_credentials_output(registry_id=repo.registry_id).apply(
lambda creds: parse_auth_token(creds.authorization_token))
image = docker.Image(
'app-img',
image_name=repo.repository_url,
# ./my-app is a folder with a Dockerfile
build=docker.DockerBuild(context='./my-app'),
registry=docker.ImageRegistry(
repo.repository_url,
creds.username,
creds.password))
package main
import (
"encoding/base64"
"fmt"
"strings"
"github.com/pulumi/pulumi-aws/sdk/v4/go/aws/ecr"
"github.com/pulumi/pulumi-docker/sdk/v3/go/docker"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type creds struct {
Username string
Password string
}
func parseAuthToken(token string) (creds, error) {
decoded, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return creds{}, err
}
parts := strings.Split(string(decoded), ":")
if len(parts) != 2 {
return creds{}, fmt.Errorf("Failed to parse token")
}
return creds{parts[0], parts[1]}, nil
}
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
repo, err := ecr.NewRepository(ctx, "app-repo", &ecr.RepositoryArgs{})
if err != nil {
return err
}
creds := ecr.GetCredentialsOutput(ctx, ecr.GetCredentialsOutputArgs{
RegistryId: repo.RegistryId,
})
username := creds.AuthorizationToken().
ApplyT(func(token string) (string, error) {
creds, err := parseAuthToken(token)
return creds.Username, err
}).(pulumi.StringOutput)
password := creds.AuthorizationToken().
ApplyT(func(token string) (string, error) {
creds, err := parseAuthToken(token)
return creds.Password, err
}).(pulumi.StringOutput)
image, err := docker.NewImage(ctx, "app-img", &docker.ImageArgs{
Build: &docker.DockerBuildArgs{
Context: pulumi.String("./my-app"),
},
ImageName: repo.RepositoryUrl,
Registry: &docker.ImageRegistryArgs{
Server: repo.RepositoryUrl,
Username: username,
Password: password,
},
})
if err != nil {
return err
}
ctx.Export("imageName", image.ImageName)
return nil
})
}
class MyStack : Pulumi.Stack
{
public MyStack()
{
var repo = new Repository("app-repo");
var creds = GetCredentials.Invoke(new GetCredentialsInvokeArgs
{
RegistryId = repo.RegistryId
});
var username = creds
.Apply(c => ParseAuthToken(c.AuthorizationToken).Username);
var password = creds
.Apply(c => ParseAuthToken(c.AuthorizationToken).Password);
var image = new Image("app-img", new ImageArgs
{
ImageName = repo.RepositoryUrl,
Build = new DockerBuild
{
// ./my-app is a folder with a Dockerfile
Context = "./my-app"
},
Registry = new ImageRegistry
{
Server = repo.RepositoryUrl,
Username = username,
Password = password,
}
});
}
public (string Username, string Password) ParseAuthToken(string token)
{
var parts = Encoding.UTF8.GetString(
Convert.FromBase64String(token)).Split(":");
return (Username: parts[0], Password: parts[1]);
}
}
Prior to the ability to call aws.ecr.getCredentials
directly with an
Output
this program required an apply
form and was a lot more
verbose and harder to read:
const creds = appRepo.id
.apply(id => aws.ecr.getCredentials({registryId: id})
creds = app_repo.id.apply(lambda repo_id: ecr.get_credentials(registry_id=repo_id))
creds := repo.ID().ToStringOutput().
ApplyT(func(id string) *ecr.GetCredentialsResult {
creds, err := ecr.GetCredentials(ctx,
&ecr.GetCredentialsArgs{RegistryId: id})
if err != nil {
panic(err)
}
return creds
}).(ecr.GetCredentialsResultOutput)
var creds = repo.Id.Apply(repoId =>
GetCredentials.InvokeAsync(new GetCredentialsArgs
{
RegistryId = repoId
}));
More examples
The above example is one of many practical situations where mixing function calls and resources benefits from the new form. To find out more, check out the following updated Pulumi examples:
Compatibility
To keep existing Pulumi programs working without changes, the function
forms are added as separate functions or methods in each
Pulumi-supported language following a simple naming convention. To
illustrate with the getCredentials
function:
Language | Existing non-Output form | New Output form |
---|---|---|
TypeScript | aws.ecr.getCredentials | aws.ecr.getCredentialsOutput |
Python | aws.ecr.get_credentials | aws.ecr.get_credentials_output |
Go | ecr.getCredentials | ecr.getCredentialsOutput |
C# | GetCredentials.InvokeAsync | GetCredentials.Invoke |
Note that there are cases where the existing non-Output
form may
still be the right choice. For example, retrieving the default VPC in
Python utilizing the existing form is simpler as it returns a result
that can be immediately inspected:
default_vpc = aws.ec2.get_vpc(default=True)
print(default_vpc.id)
Prefer the new Output form when passing resource outputs to a function
or else using the outputs of the function as inputs to resources. You
may still want to use the existing non-Output form if you are using
the outputs of the function to inform control flow (if
conditionals
or for
loops).
Get started
To use Output-versioned functions, please upgrade your install of Pulumi to at least 3.17.1 and upgrade your providers to the latest available version. Example compatible versions for major Pulumi providers:
Provider | Version |
---|---|
pulumi-aws | 4.27.0 |
pulumi-azure-native | 1.45.0 |
pulumi-google-native | 0.8.0 |
pulumi-azure | 4.26.0 |
pulumi-gcp | 5.26.0 |