Skip to content
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

rfc 328 - polyglot assert #330

Merged
merged 11 commits into from
Jun 11, 2021
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
335 changes: 335 additions & 0 deletions text/0328-polyglot-assert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
---
rfc pr: https://github.com/aws/aws-cdk-rfcs/pull/330
tracking issue: https://github.com/aws/aws-cdk-rfcs/issues/328
---

# Polyglot Assert

The `@aws-cdk/assert` module is only available to javascript users.

The new polyglot assert module - `@aws-cdk/assertv2` - provides the same set of functionalities
nija-at marked this conversation as resolved.
Show resolved Hide resolved
to users in all CDK supported languages.

## Working Backwards

### CHANGELOG

**feat:** announcing assertv2: the assert library is now available in all CDK supported languages.

### README

<!--BEGIN STABILITY BANNER-->

---

![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge)

---

<!--END STABILITY BANNER-->

> NOTE: This module contains *beta APIs*.
>
> All beta symbols are suffixed with the `Beta<n>`. When we have backwards
nija-at marked this conversation as resolved.
Show resolved Hide resolved
> incompatible change, we will create a new symbol with a `Beta<n+1>` suffix
> and deprecate the `Beta<n>` symbol.
---
This module allows asserting the contents of CloudFormation templates.

To run assertions based on a CDK `Stack`, start off with -

```ts
const stack = new cdk.Stack(...)
...
const inspect = StackAssertionsBeta1.fromStackBeta1(stack);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
nija-at marked this conversation as resolved.
Show resolved Hide resolved
```

Alternatively, assertions can be run on an existing CloudFormation template -

```ts
const file = '/path/to/file';
const inspect = StackAssertionsBeta1.fromTemplateFileBeta1(file);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
```

#### Full Template Match

The simplest assertion would be to assert that the template matches a given
template.

```ts
inspect.assertMatchTemplateBeta1({
nija-at marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assert everywhere seems a little redundant? Especially if we rename the inspect variable assert. How about:

Suggested change
inspect.assertMatchTemplateBeta1({
assert.matchTemplateBeta1({

(And same for assert.hasResourceBeta1 etc)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the assert prefix makes it clear that these methods will throw if the conditions are not met.

I've left space, for the future, to add a matchTemplate() that returns true/false. Maybe premature(?).

Resources: {
Type: 'Foo::Bar',
Properties: {
Baz: 'Qux',
},
},
});
```

#### Counting Resources

This module allows asserting the number of resources of a specific type found
in a template.

```ts
inspect.assertResourceCountBeta1('Foo::Bar', 2);
```

#### Resource Matching

Beyond resource counting, the module also allows asserting that a resource with
specific properties are present.

The following code asserts that the `Properties` section of a resource of type
`Foo::Bar` contains the specified properties -

```ts
inspect.assertResourceBeta1('Foo::Bar', {
Foo: 'Bar',
Baz: 5,
Qux: [ 'Waldo', 'Fred' ],
nija-at marked this conversation as resolved.
Show resolved Hide resolved
});
```

The same method allows asserting the complete definition of the 'Resource'
which can be used to verify things other sections like `DependsOn`, `Metadata`,
`DeletionProperty`, etc.

```ts
inspect.assertResourceBeta1('Foo::Bar', {
Properties: { Foo: 'Bar' },
DependsOn: [ 'Waldo', 'Fred' ],
}, {
part: ResourcePartBeta1.COMPLETE_BETA1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, no need to add _BETA1 everywhere. It's sufficient that the top level type has that prefix...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say I wanted to change the behaviour of this enum value. With your suggestion I would need to create a new enum ResourcePartBeta2 and then create new methods with Beta2 (or whatever) suffix everywhere this enum is used.

More pragmatic to keep the suffix, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I can think of a reason you'd want to change the value of the enum member. It's normally not even needed to specify a value.

We should be consistent about these suffixes. If the entire type is "Beta", I don't see any value in actually polluting all the members with that suffix.

Copy link
Contributor

@NetaNir NetaNir Jun 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To @nija-at point, if we only use betaX on the containing type (e.g class), what do we do when we want to deprecate an internal member? For example, assume the autoScalingGroup class is in preview phase, and we only want to change the behavior of formAutScalingGroupName method, do we need to deprecate the entire autoScalingGroup class for that?

export class AutoScalingGroupBeta  {

   public static fromAutoScalingGroupName(scope: Construct, id: string, autoScalingGroupName: string): 
    .....
  }
}

My gut feeling is that it will be less ideal from a user experience perspective.

there are a lot of nuance here, I think we need to flush out the preview specification so we can be consistent across the code base.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just deprecate AutoScalingGroupBeta, and introduce AutoScalingGroupBeta2 with this new method, while proxying all the deprecated methods from the deprecated type to the new type.

Copy link
Contributor

@NetaNir NetaNir Jun 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will this work for users consuming fromAutoScalingGroupName? If you proxy it, you will be introducing a breaking change for users of AutoScalingGroupBeta

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout. Of course this specific method will not be proxied as-is in order to preserve the legacy behavior in the deprecated class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with Elad here. Having to call fromAutoScalingGroupName() AutoScalingGroupBeta1.fromAutoScalingGroupNameBeta1() on the off-chance we will want to deprecate it seems like the wrong tradeoff to me.

Also, let's assume for a second we do go down this route, and call it AutoScalingGroupBeta1.fromAutoScalingGroupNameBeta1(). What do we call the replacement method then? AutoScalingGroupBeta1.fromAutoScalingGroupNameBeta2()? That looks pretty bad...

Copy link
Contributor

@NetaNir NetaNir Jun 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are over indexing on how things look. Our main objective is to make sure it is clear that an API is in preview. The second objective is to make is easy for users to use preview APIs, will introducing a whole new class will make it harder for users to upgrade? It can be, as the class usage is more prevalent than a single method in a user application (more references to the AutoScalingGroup class in the code base than to fromAutoScalingGroupName).

I think we need to make this decision in a dedicated PR, try out a few use cases and flush out the details using code samples.

});
nija-at marked this conversation as resolved.
Show resolved Hide resolved
```

nija-at marked this conversation as resolved.
Show resolved Hide resolved
## FAQ

### What are we launching today?

Today, we are launching a new jsii module called `@aws-cdk/assertv2`, that allows
CDK users to run assertions on the synthesized CloudFormation template.

This module is available to users in all CDK supported languages.

### Why should I use this feature?

You should use this feature if you would like to ensure that your CDK constructs
and CDK applications render expected CloudFormation resources and properties.

### How is this different from the existing `assert` module?

The two modules provide the same set of features. However, the existing module
was only available to javascript users.
The new version launched today is available to users of all CDK supported
languages available today and any that we will support in the future.

nija-at marked this conversation as resolved.
Show resolved Hide resolved
### How do I get started?

This module is available in the languages as per the table below -

| Language | Pkg Manager | Package |
| --------------------- | ------------|-------------------------------------------- |
| typescript/javascript | npm | `@aws-cdk/assertv2` |
| Java | Maven | `software.amazon.awscdk.assertv2` |
| Python | PyPI | `aws-cdk.assertv2` |
| .NET | NuGet | `Amazon.CDK.AssertV2` |
| Go | - | `github.com/aws/aws-cdk-go/awscdk/assertv2` |

### Is this feature available in the AWS CDK v2?

This module is available in both the AWS CDK v1 and v2, just like all of our other
modules.

In CDKv2, this module will be available as part of `aws-cdk-lib`.
The import statement you will need to use is -

```ts
import { assertv2 as assert } from 'aws-cdk-lib';
nija-at marked this conversation as resolved.
Show resolved Hide resolved
nija-at marked this conversation as resolved.
Show resolved Hide resolved
```

Read more about `aws-cdk-lib` at
<https://github.com/aws/aws-cdk/tree/master/packages/aws-cdk-lib#readme>.

### Why are all the APIs suffixed with the `Beta1` suffix?
nija-at marked this conversation as resolved.
Show resolved Hide resolved
nija-at marked this conversation as resolved.
Show resolved Hide resolved

We have promised that there will be no backwards incompatible or breaking changes
to our APIs in `aws-cdk-lib` as part of the AWS CDK v2.

However, this module is still under development and is a developer preview release,
where we are collecting feedback from users and performing necessary improvements.

When an API needs to be modified in backwards incompatible ways, we will create a
new API with a new suffix (`Beta2`, etc.). The old APIs will continue to be work but
will be marked as deprecated.

When the module is ready for prime time, we will publish the APIs without the 'Beta'
suffix and mark all 'Beta' APIs as deprecated.

## Internal FAQ

### Why are we doing this?

We have received a lot of requests and feedback from users that they would like to use
our 'assert' library in the languages of their choice, primarily Python users.

### Why should we _not_ do this?

None.

### What changes are required to enable this change?

The high level design is to create a new jsii module in the AWS CDK monorepo -
`@aws-cdk/assertv2` - that exposes the APIs proposed above.

The implementation of this module would simply be a scaffold around the existing
`assert` library.

To build this scaffold, we will 'vendor in' the libraries `assert-internal`,
`cloudformation-diff` and `cfnspec`.

See [Appendix A](#appendix-a---detailed-design) for the design and rationale.

Prototype / Implementation: <https://github.com/aws/aws-cdk/pull/14952>.

### Is this a breaking change?

This is not a breaking change.

### What are the drawbacks of this solution?

1. Ideally, the dependencies, `assert`, `cloudformation-diff` and `cfnspec`
are all available to be consumed as regular dependencies but are not,
since they are part of the same monorepo.

Instead, the design 'vendors in' these dependencies, i.e., copies over
the source code and rewrites imports, thereby increasing tech debt.

2. Any new feature or bug fix to the new assert module needs to be performed
nija-at marked this conversation as resolved.
Show resolved Hide resolved
both in the old assert module and new. This can lead to weird/complicated
implementation, since they both follow different design patterns.

3. The 'jest' friendly interfaces that were previously available are no longer
nija-at marked this conversation as resolved.
Show resolved Hide resolved
available.
Users previously using -

```ts
expect(stack).toHaveResource('Foo::Bar', { ... });
```

will now have to use

```ts
const assertions = StackAssertions.fromStack(stack);
nija-at marked this conversation as resolved.
Show resolved Hide resolved
assertions.assertResource('Foo::Bar', { ... });
nija-at marked this conversation as resolved.
Show resolved Hide resolved
```

### What are the mitigations and/or long term plans to address these concerns?

Once this module reaches general availability, we will deprecate the old 'assert'
module. At that point, all of its source code can now be made as part of the new
'assert' module.

The dependencies on 'cloudformation-diff' and 'cfnspec' will need to be carefully
evaluated and a decision made on whether to lift-and-shift the relevant code, or
to move these out of the monorepo as independent modules.

Friendly interfaces for popular testing frameworks such as jest, nunit, junit, etc.
can be built as separate modules specific to those languages (not jsii modules)
on top of the new 'assert' library.
If necessary, this can be owned and maintained by the CDK team, but probably more
suited for the community to pick up and build as third party modules.
nija-at marked this conversation as resolved.
Show resolved Hide resolved

### What alternative solutions did you consider?

See [Appendix B](#appendix-b---alternatives) for alternatives.

### What is the high level implementation plan?

The implementation plan is fairly simple.

- Implement and release the new assert module.
- Migrate modules to use the new assert module.

### Are there any open issues that need to be addressed later?

None.

## Appendix A - Detailed Design

### Key Requirement

The **key requirement** for the design choices is *dogfooding*.
The new assert APIs must be usable from within the AWS CDK, and over time,
all CDK modules that use the old assert module should be migrated to the new one.

This is crucial to keep the assert library active and maintained.
This is reinforced by the old assert library which, over time, has evolved with
new features added to it.

### Ship Vehicle

The assert library works on `Stack` which is a construct available in `@aws-cdk/core`
(or `aws-cdk-lib` in v2). Hence, takes a dependency on this construct.

Given this and working backwards from the key requirement, the new assert module
must be part of the AWS CDK monorepo.
Comment on lines +352 to +353
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic doesn't convince me.

Why can't this be a separate module that depends on aws-cdk-lib?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you satisfy the requirement I've identified (in the above section) if this is a separate module?
Do you disagree with that requirement?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can ship a separate experimental assert library from a module in the monorepo, re-writing it as needed at build time.

Same way as we re-write the stable modules today to package them as part of monocdk, and the same way as we plan to ship the existing experimental modules like @aws-cdk/aws-appmesh (which has version 1.107.0) as new modules @aws-cdk-lib-experiments/aws-appmesh (with version 0.1.0).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll point you again towards the requirements section and quote it here, because your reply seems to ignore it -

The key requirement for the design choices is dogfooding.
The new assert APIs must be usable from within the AWS CDK, and over time,
all CDK modules that use the old assert module should be migrated to the new one.

If we ship this as an experimental module, I believe we cannot take declare a dependency on it from any of our stable modules.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we ship this as an experimental module, I believe we cannot take declare a dependency on it from any of our stable modules.

Pretty sure we can, because it will be a devDependency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why deal with all this complexity? If we just publish this as any CDK construct library, it keeps things simple and gets the job done. We can deal with it the same way we handle everything else.
Why do you not like shipping this as any other module? The only difference I see is that these does not participate in synthesis, which is acceptable.

Because it bloats the aws-cdk-lib module with test code, and forces you to use the ugly Beta1 names.

I honestly don't see the problem here. We will have a module in the monorepo, @aws-cdk/assertions or something, which the single (v1) modules will depend on. This takes care of dogfooding. Then we re-package the module to be released with whatever name we want, @aws-cdk-lib/assertions or something else, in version 0.x.y. It will peerDepend on aws-cdk-lib.

This is exactly the process we want to have for other experimental modules.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Thanks for the feedback.

I'll take that into consideration before my final decision.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skinny85 I am not sure I understand your arguments against including this is the standard library (besides module size, which is not a very strong argument given this is likely to add a few KiBs).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also allows us to skip the ugly Beta1 suffixes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair argument.
If we can release this library as an experimental module without adding any further complexity, we can do so.

However, after graduating to stable, the module will be included in aws-cdk-lib (just like any other experimental module).


### Dependencies

The main dependencies of the new assert module are the existing `assert` module,
which in turn mainly depend on `cloudformation-diff` and `cfnspec`.
The latter two are required to display human readable diffs, around what resources
are different and how IAM permissions have changed.

All three of these modules are pure typescript, and not jsii modules, whereas the
new assert module is a jsii module.
jsii only allows other jsii modules as regular dependencies, and any non jsii module
should be [bundled](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#bundleddependencies).

However, all three modules are within the AWS CDK monorepo and the tooling of `yarn`,
`lerna` and `npm` do not support bundling modules that are within the monorepo.

For this reason, we have gone with the option to 'vendor in' these modules by copying
the source directly into the new assert module with some minor adjustments.
nija-at marked this conversation as resolved.
Show resolved Hide resolved

### CDK v2 Availability

In CDK v2, `aws-cdk-lib` only packages stable modules. Modules marked experimental
are published as a separate module. There is a strict requirement imposed by the
`aws-cdk-lib` packaging mechanism that it cannot depend on any of the experimental
modules.

To adhere to these constraints, the new assert module is marked as 'stable' and
we will use the experimental API feature, i.e., suffix all APIs with `Beta<n>`.

## Appendix B - Alternatives

### Alternative 1 - Remove the dependency on `Stack`

If we removed the dependency of the assert library on `Stack`, then we can release
this module separately from the monorepo, and consume it in the AWS CDK as any other
`devDependency`.

However, this will unearth more problems.

Firstly, the dependency on `Stack` really comes from the original 'assert' module.
Removing this is not trivial and is a breaking change on the old assert library.

Further, doing so will spoil the API ergnomoics of the assert module, even though it
will remain usable.

Moreover, we are still left with dependencies on `cloudformation-diff` and `cfnspec`
which are part of the monorepo. A set up like this will leave us in a situation where
we are using older versions of these packages in the dependency closure, even though
the latest version is present in the monorepo.

### Alternative 2 - Bundle the dependencies

As mentioned in the original design, the existing tooling of `yarn`, `lerna` and
`npm` do not support bundling packages that are part of the monorepo.

We can build such tooling ourselves, but this will be more tooling we will need to
maintain.