Skip to content

Commit

Permalink
changes to docs for 0.3, first draft (#447)
Browse files Browse the repository at this point in the history
* changes to docs for 0.3.x
* updated changelog for 0.3.0
  • Loading branch information
lbialy authored Apr 11, 2024
1 parent 5e67e0d commit 08708e6
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 120 deletions.
22 changes: 22 additions & 0 deletions codegen/src/CodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,31 @@ class CodeGen(using

val unionDecoders = unionDecoderGivens(resourceProperties)

def argsDefaultMsg = if argsDefault.isEmpty then "" else "This resource has a default configuration."

val baseCompanion =
if (hasOutputExtensions) {
m"""|object $baseClassName extends besom.ResourceCompanion[$baseClassName]:
| /** Resource constructor for $baseClassName.
| *
| * @param name [[besom.util.NonEmptyString]] The unique (stack-wise) name of the resource in Pulumi state (not on provider's side).
| * NonEmptyString is inferred automatically from non-empty string literals, even when interpolated. If you encounter any
| * issues with this, please try using `: NonEmptyString` type annotation. If you need to convert a dynamically generated
| * string to NonEmptyString, use `NonEmptyString.apply` method - `NonEmptyString(str): Option[NonEmptyString]`.
| *
| * @param args [[${argsClassName}]] The configuration to use to create this resource. $argsDefaultMsg
| *
| * @param opts [[${resourceOptsClass}]] Resource options to use for this resource.
| * Defaults to empty options. If you need to set some options, use [[besom.opts]] function to create them, for example:
| *
| * {{{
| * val res = $baseClassName(
| * "my-resource",
| * ${argsClassName}(...), // your args
| * opts(provider = myProvider)
| * )
| * }}}
| */
| def apply(using ctx: besom.types.Context)(
| name: besom.util.NonEmptyString,
| args: ${argsClassName}${argsDefault},
Expand Down
40 changes: 40 additions & 0 deletions codegen/src/CodeGen.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ class CodeGenTest extends munit.FunSuite {
|) extends besom.ProviderResource
|
|object Provider extends besom.ResourceCompanion[Provider]:
| /** Resource constructor for Provider.
| *
| * @param name [[besom.util.NonEmptyString]] The unique (stack-wise) name of the resource in Pulumi state (not on provider's side).
| * NonEmptyString is inferred automatically from non-empty string literals, even when interpolated. If you encounter any
| * issues with this, please try using `: NonEmptyString` type annotation. If you need to convert a dynamically generated
| * string to NonEmptyString, use `NonEmptyString.apply` method - `NonEmptyString(str): Option[NonEmptyString]`.
| *
| * @param args [[ProviderArgs]] The configuration to use to create this resource. This resource has a default configuration.
| *
| * @param opts [[besom.CustomResourceOptions]] Resource options to use for this resource.
| * Defaults to empty options. If you need to set some options, use [[besom.opts]] function to create them, for example:
| *
| * {{{
| * val res = Provider(
| * "my-resource",
| * ProviderArgs(...), // your args
| * opts(provider = myProvider)
| * )
| * }}}
| */
| def apply(using ctx: besom.types.Context)(
| name: besom.util.NonEmptyString,
| args: ProviderArgs = ProviderArgs(),
Expand Down Expand Up @@ -204,6 +224,26 @@ class CodeGenTest extends munit.FunSuite {
| ctx.call[besom.api.googlenative.container.v1.ClusterGetKubeconfigArgs, besom.api.googlenative.container.v1.ClusterGetKubeconfigResult, besom.api.googlenative.container.v1.Cluster]("google-native:container/v1:Cluster/getKubeconfig", args, this, opts)
|
|object Cluster extends besom.ResourceCompanion[Cluster]:
| /** Resource constructor for Cluster.
| *
| * @param name [[besom.util.NonEmptyString]] The unique (stack-wise) name of the resource in Pulumi state (not on provider's side).
| * NonEmptyString is inferred automatically from non-empty string literals, even when interpolated. If you encounter any
| * issues with this, please try using `: NonEmptyString` type annotation. If you need to convert a dynamically generated
| * string to NonEmptyString, use `NonEmptyString.apply` method - `NonEmptyString(str): Option[NonEmptyString]`.
| *
| * @param args [[ClusterArgs]] The configuration to use to create this resource. This resource has a default configuration.
| *
| * @param opts [[besom.CustomResourceOptions]] Resource options to use for this resource.
| * Defaults to empty options. If you need to set some options, use [[besom.opts]] function to create them, for example:
| *
| * {{{
| * val res = Cluster(
| * "my-resource",
| * ClusterArgs(...), // your args
| * opts(provider = myProvider)
| * )
| * }}}
| */
| def apply(using ctx: besom.types.Context)(
| name: besom.util.NonEmptyString,
| args: ClusterArgs = ClusterArgs(),
Expand Down
4 changes: 2 additions & 2 deletions website/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
title: Overview
---


Pulumi runtime is **asynchronous by design**. The goal is to allow the user's program to declare all the necessary resources
as fast as possible so that Pulumi engine can make informed decisions about which parts of the deployment plan can be
executed in parallel and therefore yield good performance.
Expand Down Expand Up @@ -30,9 +29,10 @@ Besom stands alone in this choice and due to it **has some differences** in comp

Following sections explain and showcase said differences:

- [Resource constructors](constructors.md) - resource constructors are pure functions that return Outputs
- [Context](context.md) - context is passed around implicitly via Scala's Context Function
- [Exports](exports.md) - your program is a function that returns Stack along with its Stack Outputs
- [Inputs and Outputs](io.md) - Outputs are static or dynamic properties passed to Inputs to configure resources
- [Resource constructors](constructors.md) - resource constructors are pure functions that return Outputs
- [Laziness](laziness.md) - dangling resources are possible and resource constructors are memoized
- [Apply method](apply_methods.md) - use `map` and `flatMap` to compose Outputs, not `apply`
- [Logging](logging.md) - all logging statements need to be composed into the main flow
Expand Down
162 changes: 162 additions & 0 deletions website/docs/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
title: Resource constructor asynchronicity
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Resources in Besom have an interesting property related to the fact that Pulumi's runtime is asynchronous.
One could suspect that in following snippet resources are created sequentially due to monadic syntax:

```scala
for
a <- aws.s3.Bucket("first")
b <- aws.s3.Bucket("second")
yield ()
```

This isn't true. Pulumi expects that a language SDK will declare resources as fast as possible. Due to this
fact resource constructors return immediately after they spawn a Resource object. A resource object is just a
plain case class with each property expressed in terms of Outputs. The work necessary for resolution of these
properties is executed asynchronously. In the example above both buckets will be created in parallel.

Given that a piece of code is worth more than a 1000 words, below you can find code snippets that explain these
semantics using known Scala technologies. In each of them `Output` is replaced with a respective async datatype
to explain what internals of Besom are actually doing when resource constructors are called (oversimplified a
bit).

<Tabs>
<TabItem value="Future" label="stdlib Future" default>

```scala
// internal function, here just to represent types
def resolveResourceAsync(name: String, args: Args, promises: Promise[_]*): Future[Unit] = ???

// resource definition
case class Bucket(bucket: Future[String])
object Bucket:
def apply(name: String, args: BucketArgs = BucketArgs()): Future[Bucket] =
// create a promise for bucket property
val bucketNamePromise = Promise[String]()
// kicks off async resolution of the resource properties
resolveResourceAsync(name, args, bucketNamePromise)
// returns immediately
Future.successful(Bucket(bucketNamePromise.future))

// this just returns a Future[Unit] that will be resolved immediately
for
a <- Bucket("first")
b <- Bucket("second")
yield ()
```

</TabItem>
<TabItem value="ce" label="Cats Effect IO">

```scala
// internal function, here just to represent types
def resolveResourceAsync(name: String, args: Args, promises: Deferred[IO, _]*): IO[Unit] = ???

// resource definition
case class Bucket(bucket: IO[String])
object Bucket:
def apply(name: String, args: BucketArgs = BucketArgs()): IO[Bucket] =
for
// create a deferred for bucket property
bucketNameDeferred <- Deferred[IO, String]()
// kicks off async resolution of the resource properties
_ <- resolveResourceAsync(name, args, bucketNameDeferred).start
yield Bucket(bucketNameDeferred.get) // returns immediately

// this just returns a IO[Unit] that will be resolved immediately
for
a <- Bucket("first")
b <- Bucket("second")
yield ()
```

</TabItem>
<TabItem value="zio" label="ZIO">

```scala
// internal function, here just to represent types
def resolveResourceAsync(name: String, args: Args, promises: Promise[_]*): Task[Unit] = ???

// resource definition
case class Bucket(bucket: Task[String])
object Bucket:
def apply(name: String, args: BucketArgs = BucketArgs()): Task[Bucket] =
for
// create a promise for bucket property
bucketNamePromise <- Promise.make[Exception, String]()
// kicks off async resolution of the resource properties
_ <- resolveResourceAsync(name, args, bucketNameDeferred).fork
yield Bucket(bucketNameDeferred.await) // returns immediately

// this just returns a Task[Unit] that will be resolved immediately
for
a <- Bucket("first")
b <- Bucket("second")
yield ()
```

</TabItem>
</Tabs>

:::info
A good observer will notice that all these computations started in a fire-and-forget fashion have to be awaited somehow
and that is true. Besom does await for all spawned Outputs to be resolved before finishing the run via a built-in task
tracker passed around in `Context`.
:::

There is an explicit way to inform Pulumi engine that some of the resources have to be created, updated or
deleted sequentially. To do that, one has to pass [resource options](https://www.pulumi.com/docs/concepts/options/)
to adequate resource constructors with `dependsOn` property set to resource to await for. Here's an example:

```scala
for
a <- Bucket("first")
b <- Bucket("second", BucketArgs(), opts(dependsOn = a))
yield ()
```

There's also `deletedWith` property that allows one to declare that some resources will get cleaned up when another
resource is deleted and that trying to delete them *after* that resource is deleted will fail. A good example of such
relationship might be Kubernetes, where deletion of a namespace takes down all resources that belong do that namespace.

This manual wiring is only necessary for cases when there are no data dependencies between defined resources. In a case
like this:

```scala
val defaultMetadata = k8s.meta.v1.inputs.ObjectMetaArgs(
labels = Map("app" -> "my-app")
)

val deployment = k8s.apps.v1.Deployment(
name = "my-app-deployment",
k8s.apps.v1.DeploymentArgs(
metadata = defaultMetadata,
spec = k8s.apps.v1.inputs.DeploymentSpecArgs(
// removed for brevity
)
)
)

val service = k8s.core.v1.Service(
name = "my-app-service",
k8s.core.v1.ServiceArgs(
spec = k8s.core.v1.inputs.ServiceSpecArgs(
selector = appLabels,
// removed for brevity
),
metadata = defaultMetadata
),
opts(dependsOn = deployment)
)
```

there is no data dependency between kubernetes deployment and kubernetes service because kubernetes links these
entities using labels. There's a guarantee that service points towards the correct deployment because a programming
language is being used and that allows to define a common constant value that is reused to define them. There is,
however, no output property of Deployment used in definition of Service and therefore Pulumi engine can't infer
that it should actually wait with the creation of Service until Deployment is up. In such cases one can use
`dependsOn` property to inform the engine about such a relationship between resources.
11 changes: 7 additions & 4 deletions website/docs/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ Inputs and Outputs are the
primary [asynchronous data types in Pulumi](https://www.pulumi.com/docs/concepts/inputs-outputs/),
and they signify values that will be provided by the engine later, when the resource is created and its properties can
be fetched.
`Input[A]` type is an alias for `Output[A]` type used by [resource](#resources) arguments.
`Input[A]` type is an alias for `Output[A]` type used by [resource](#resources) arguments. Inputs are
[very elastic](io.md/#inputs) in what they can receive to facilitate preview-friendly, declarative model of programming.

Outputs are values of type `Output[A]` and behave very much
like [monads](https://en.wikipedia.org/wiki/Monad_(functional_programming)).
Expand All @@ -229,7 +230,7 @@ Outputs are used to:

- automatically captures dependencies between [resources](#resources)
- provide a way to express transformations on its value before it's known
- deffer the evaluation of its value until it's known
- defer the evaluation of its value until it's known
- track the _secretness_ of its value

Output transformations available in Besom:
Expand All @@ -238,9 +239,11 @@ Output transformations available in Besom:
output
- [lifting](lifting.md) directly read properties off an output value
- [interpolation](interpolator.md) concatenate string outputs with other strings directly
- `sequence` method combines multiple outputs into a single output of a list
- `sequence` method combines multiple outputs into a single output of a collection (`parSequence` variant is also available
for explicit parallel evaluation)
- `zip` method combines multiple outputs into a single output of a tuple
- `traverse` method transforms a map of outputs into a single output of a map
- `traverse` method transforms a collection of values into a single output of a collection ((`parTraverse` variant is also available
for explicit parallel evaluation))

To create an output from a plain value, use the `Output` constructor, e.g.:

Expand Down
17 changes: 17 additions & 0 deletions website/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,25 @@ assert(json.parseJson.convertTo[Color] == color)

## Bug Fixes

* fixed infinite loop in encoders [#407](https://github.com/VirtusLab/besom/issues/407) when a recursive type is encountered
* fixed cause passing in AggregateException to improve debugging of decoders [#426](https://github.com/VirtusLab/besom/issues/426)
* fixed Pulumi side effects memoization issues in Component API [#429]https://github.com/VirtusLab/besom/pull/429
* fixed traverse problem caused by export bug in compiler with a temporary workaround [#430](https://github.com/VirtusLab/besom/issues/430)

## Other Changes

* custom timeouts have scaladocs now [#419](https://github.com/VirtusLab/besom/pull/419)
* overhauled serde layer with refied outputs implemented to improve parity with upstream Pulumi engine [#414](https://github.com/VirtusLab/besom/pull/414)
* StackReferences are now documented [#428](https://github.com/VirtusLab/besom/pull/428)
* updated AWS EKS hello world example [#399](https://github.com/VirtusLab/besom/pull/399/files)
* Component API now disallows returning component instances wrapped in Outputs to prevent users from dry run issues [#441](https://github.com/VirtusLab/besom/pull/441)
* added parSequence parSequence and parTraverse combinators on Output [#440](https://github.com/VirtusLab/besom/pull/440)
* added Output.when combinator [#439](https://github.com/VirtusLab/besom/pull/439)
* improved compilation errors around `Output.eval` and `Output#flatMap` [#443](https://github.com/VirtusLab/besom/pull/443)
* all Output combinators have scaladocs now [#445](https://github.com/VirtusLab/besom/pull/445)
* added extension-based combinators for `Output[Option[A]]`, `Output[List[A]]` etc [#445](https://github.com/VirtusLab/besom/pull/445)
* added support for overlays (package-specific extensions) in besom codegen, this opens a way for support of Helm, magic lambdas and other advanced features [#402](https://github.com/VirtusLab/besom/pull/402)

**Full Changelog**: https://github.com/VirtusLab/besom/compare/v0.2.2...v0.3.0

0.2.2 (22-02-2024)
Expand Down
Loading

0 comments on commit 08708e6

Please sign in to comment.