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:

LanguageExisting non-Output formNew Output form
TypeScriptaws.ecr.getCredentialsaws.ecr.getCredentialsOutput
Pythonaws.ecr.get_credentialsaws.ecr.get_credentials_output
Goecr.getCredentialsecr.getCredentialsOutput
C#GetCredentials.InvokeAsyncGetCredentials.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:

ProviderVersion
pulumi-aws4.27.0
pulumi-azure-native1.45.0
pulumi-google-native0.8.0
pulumi-azure4.26.0
pulumi-gcp5.26.0