Skip to main content

Inputs and Outputs

Outputs are the primary asynchronous data structure of Pulumi programs.

Outputs

Outputs are:

  • pure and lazy - meaning that they suspend evaluation of code until interpretation, which is perfomed by Besom runtime that runs Pulumi.run function at the, so called, end-of-the-world.

  • monadic - meaning that they expose map and flatMap operators and can be used in for-comprehensions

Outputs are capable of consuming other effects for which there exists an instance of ToFuture typeclass. Besom provides 3 such instances:

  • package besom-core provides an instance for scala.concurrent.Future
  • package besom-cats provides an instance for cats.effect.IO
  • package besom-zio provides an instance for zio.Task

Inputs

Inputs are Besom types used wherever a value is expected to be provided by user primarily to ease the use of the configuration necessary for resource constructors to spawn infrastructure resources. Inputs allow user to provide both raw values, values that are wrapped in an Output, both of the former or nothing at all when dealing with optional fields or even singular raw values or lists of values for fields that expect multiple values.

To make this more digestable - the basic Input[A] type is declared as:

opaque type Input[+A] >: A | Output[A] = A | Output[A]

ane the Input.Optional[A] variant is declared as:

opaque type Optional[+A] >: Input[A | Option[A]] = Input[A | Option[A]]

This allows for things like this:

val int1: Input[Int] = 23
val int2: Input[Int] = Output(23)
// what if it's an optional value?
val maybeInt1: Input.Optional[Int] = 23
val maybeInt2: Input.Optional[Int] = None
val maybeInt3: Input.Optional[Int] = Some(23)
// yes, but also:
val maybeInt4: Input.Optional[Int] = Output(23)
val maybeInt5: Input.Optional[Int] = Output(Option(23))
val maybeInt6: Input.Optional[Int] = Output(None)

This elastic and permissive model was designed to allow a more declarative style and facilitate the implicit parallelism of evaluation. In fact, Outputs are meant to be thought of as short pipelines that one uses to transform properties and values obtained from one resource to be used as argument for another. If you're used to the classic way of working with monadic programs with chains of flatMap and map or for-comprehensions this might seem a bit odd to you - why would we take values wrapped in Outputs as arguments? The answer is: previews!

Outputs incorporate semantics of Option to support Pulumi's preview / dry-run feature that allows one to see what changes will be applied when the program is executed against the actual environment. This, however, means that Outputs can short-circuit when a computed (provided by the engine) value is missing in dry-run and most properties on resources belong to this category. It is entirely possible to structure a Besom program the same way one would structure a program that uses Cats Effect IO or ZIO but once you flatMap on an Output value that can be only obtained from actual environment short-circuiting logic will kick in and all the subsequent flatMap/map steps will be skipped yielding a broken view of the changes that will get applied in your next change to the infrastructure. To avoid this problem it is highly recommended to write Besom programs in a style highly reminiscent of direct style and use for-comprehensions only to transform properties passed from configuration or declared resources to another resources. This way the graph of resources is fully known in dry-run phase and can be properly inspected. Full power of monadic composition should be reserved for situations where it is strictly necessary.

info

We are working on a solution that would allow us to track computed Output values on the type level and therefore inform the user (via a compile-time information or warning) that a dynamic subtree of resources will be spawned by their code that won't be visible in preview.