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

feat(cfn-include): preserve properties of resources that are not in the current CFN schema #11822

Merged
merged 8 commits into from
Dec 4, 2020

Conversation

skinny85
Copy link
Contributor

@skinny85 skinny85 commented Dec 2, 2020

Currently, if cloudformation-include encounters a property in one of the resources the CDK has an L1 for
that is not in the version of the CloudFormation resource schema the module uses,
the property will be silently dropped. Of course, that is not ideal.

Make changes to our L1 code generation to preserve all properties that are not in the CFN schema.

Fixes #9717


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@skinny85 skinny85 requested a review from rix0rrr December 2, 2020 02:13
@skinny85 skinny85 self-assigned this Dec 2, 2020
@gitpod-io
Copy link

gitpod-io bot commented Dec 2, 2020

@github-actions github-actions bot added the @aws-cdk/cloudformation-include Issues related to the "CFN include v.20" package label Dec 2, 2020
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Dec 2, 2020
@skinny85
Copy link
Contributor Author

skinny85 commented Dec 2, 2020

s3.generated.ts with the changes from this PR: https://gist.github.com/skinny85/b892306a9b027fc8738395a0dfcee4d6

Copy link
Contributor

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

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

Money question I have after all of this:

Why not use CFN overrides to remember the properties and make sure they're rendered back out?

* @returns a new object with all the entries in `object`,
* except those whose keys are one of the names provided in `props`
*/
public static omit(object: object, ...props: string[]): object {
Copy link
Contributor

Choose a reason for hiding this comment

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

The location of this function is a tough un' eh.

It strictly speaking has nothing to do with FromCloudFormation, so it should be a generic Util function. But on the other hand, it is only used there and it's nice and convenient to put it here. Oh well, I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it was put here more for convenience than anything else.

The only other place I can think of that makes sense for it is in runtime.ts in @aws-cdk/core.

Let me know what you think!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think runtime.ts might make for a good location, yeah.

tools/cfn2ts/lib/codegen.ts Outdated Show resolved Hide resolved
@skinny85 skinny85 force-pushed the feat/cfn-include-missing-props branch 2 times, most recently from 1326cb8 to 2e26abc Compare December 3, 2020 00:45
@skinny85 skinny85 added the pr-linter/exempt-readme The PR linter will not require README changes label Dec 3, 2020
@skinny85
Copy link
Contributor Author

skinny85 commented Dec 3, 2020

Money question I have after all of this:

Why not use CFN overrides to remember the properties and make sure they're rendered back out?

Excellent question. There is a reason, and it's nested properties.

While adding the property overrides would work for top-level properties, like this (present in the test of the initial version of this PR):

{
  "Resources": {
    "Bucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "PropertyNotInCfnSchema": 1
      }
    }
  }
}

It would not work for unknown properties in nested complex types, like this:

{
  "Resources": {
    "Bucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "AccelerateConfiguration": {
          "AccelerationStatus": "Enabled",
          "PropertyNotInCfnSchema": false
        }
      }
    }
  }
}

For that second case, we need to save the unrecognized property in the converters generated for each of the complex property types of the resource. I don't think that can be reasonably achieved with property overrides without huge changes to the current code generator.

(BTW, I've added this second case to the tests in this PR)

@skinny85 skinny85 requested a review from rix0rrr December 3, 2020 01:43
@@ -264,7 +264,7 @@ export class Distribution extends Resource implements IDistribution {
}

const originId = this.addOrigin(props.defaultBehavior.origin);
this.defaultBehavior = new CacheBehavior(originId, { pathPattern: '*', ...props.defaultBehavior });
this.defaultBehavior = new CacheBehavior(originId, props.defaultBehavior);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change safe?

@rix0rrr rix0rrr added the pr/do-not-merge This PR should not be merged at this time. label Dec 3, 2020
Copy link
Contributor

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

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

It would not work for unknown properties in nested complex types, like this:

But it would, as far as I'm aware (or at least it should). Properties can be dot-separated to indicate deep properties, like so:

cfnResource.addPropertyOverride('AccelerateConfiguration.PropertyNotInCfnSchema', false);

Am I missing something?

* @returns a new object with all the entries in `object`,
* except those whose keys are one of the names provided in `props`
*/
public static omit(object: object, ...props: string[]): object {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think runtime.ts might make for a good location, yeah.

…he current CFN schema

Currently, if cloudformation-include encounters a property in one of the resources the CDK has an L1 for
that is not in the version of the CloudFormation resource schema the module uses,
the property will be silently dropped. Of course, that is not ideal.

Make changes to our L1 code generation to preserve all properties that are not in the CFN schema.

Fixes aws#9717
@skinny85 skinny85 force-pushed the feat/cfn-include-missing-props branch from 6a9da5e to 1c98223 Compare December 4, 2020 01:51
@skinny85 skinny85 requested a review from rix0rrr December 4, 2020 01:51
@skinny85
Copy link
Contributor Author

skinny85 commented Dec 4, 2020

@rix0rrr this is ready for another review. I've changed to using property overrides, like we discussed. I kind of hate how the code looks because of that, especially the changes done in FromCloudFormation static methods from cfn-parse.ts. But let me know what you think.

The s3.generated.ts file generated with the changes from this PR: https://gist.github.com/skinny85/ed4dc3556c95b343f958f2e5e9689b44

@rix0rrr
Copy link
Contributor

rix0rrr commented Dec 4, 2020

I agree that it currently doesn't look great. On the one hand, it's generated code so I really don't care what it looks like, only that it does the right thing.

But I see what you mean. We have to do 2 things (instead of 1) to the return value of every inner parser, which requires that we declare a variable to hold the intermediary result.

But what if we defined an additional function which does the 2 things that we need to a parameter? Then it becomes one line per property again, which is rather clean.


I added in one more convenience class which is intended to be used as an accumulator. It cleans this:

function CfnAccessPointPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult<CfnAccessPointProps> {
    properties = properties || {};
    const bucketResult = cfn_parse.FromCloudFormation.getString(properties.Bucket);
    const creationDateResult = properties.CreationDate != null ? cfn_parse.FromCloudFormation.getString(properties.CreationDate) : undefined;
    const nameResult = properties.Name != null ? cfn_parse.FromCloudFormation.getString(properties.Name) : undefined;
    const networkOriginResult = properties.NetworkOrigin != null ? cfn_parse.FromCloudFormation.getString(properties.NetworkOrigin) : undefined;
    const policyResult = properties.Policy != null ? cfn_parse.FromCloudFormation.getAny(properties.Policy) : undefined;
    const policyStatusResult = properties.PolicyStatus != null ? cfn_parse.FromCloudFormation.getAny(properties.PolicyStatus) : undefined;
    const publicAccessBlockConfigurationResult = properties.PublicAccessBlockConfiguration != null ? CfnAccessPointPublicAccessBlockConfigurationPropertyFromCloudFormation(properties.PublicAccessBlockConfiguration) : undefined;
    const vpcConfigurationResult = properties.VpcConfiguration != null ? CfnAccessPointVpcConfigurationPropertyFromCloudFormation(properties.VpcConfiguration) : undefined;
    const ret = new cfn_parse.FromCloudFormationResult({
        bucket: bucketResult?.value,
        creationDate: creationDateResult?.value,
        name: nameResult?.value,
        networkOrigin: networkOriginResult?.value,
        policy: policyResult?.value,
        policyStatus: policyStatusResult?.value,
        publicAccessBlockConfiguration: publicAccessBlockConfigurationResult?.value,
        vpcConfiguration: vpcConfigurationResult?.value,
    });
    ret.appendExtraProperties('Bucket', bucketResult?.extraProperties);
    ret.appendExtraProperties('CreationDate', creationDateResult?.extraProperties);
    ret.appendExtraProperties('Name', nameResult?.extraProperties);
    ret.appendExtraProperties('NetworkOrigin', networkOriginResult?.extraProperties);
    ret.appendExtraProperties('Policy', policyResult?.extraProperties);
    ret.appendExtraProperties('PolicyStatus', policyStatusResult?.extraProperties);
    ret.appendExtraProperties('PublicAccessBlockConfiguration', publicAccessBlockConfigurationResult?.extraProperties);
    ret.appendExtraProperties('VpcConfiguration', vpcConfigurationResult?.extraProperties);
    for (const [key, val] of Object.entries(cfn_parse.FromCloudFormation.omit(properties, 'Bucket', 'CreationDate', 'Name', 'NetworkOrigin', 'Policy', 'PolicyStatus', 'PublicAccessBlockConfiguration', 'VpcConfiguration')))  {
        ret.appendExtraProperty(key, val);
    }
    return ret;
}

Up to:

function CfnAccessPointPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult<CfnAccessPointProps> {
    properties = properties || {};
    const ret = new cfn_parse.FromCloudFormationPropertyObject<CfnAccessPointProps>();

    ret.addResult('bucket', 'Bucket', cfn_parse.FromCloudFormation.getString(properties.Bucket));
    ret.addResult('creationDate', 'CreationDate', properties.CreationDate != null ? cfn_parse.FromCloudFormation.getString(properties.CreationDate) : undefined);
    ret.addResult('name', 'Name', properties.Name != null ? cfn_parse.FromCloudFormation.getString(properties.Name) : undefined);
    ret.addResult('networkOrigin', 'NetworkOrigin', properties.NetworkOrigin != null ? cfn_parse.FromCloudFormation.getString(properties.NetworkOrigin) : undefined);
    ret.addResult('policy', 'Policy', properties.Policy != null ? cfn_parse.FromCloudFormation.getAny(properties.Policy) : undefined);
    ret.addResult('policyStatus', 'PolicyStatus', properties.PolicyStatus != null ? cfn_parse.FromCloudFormation.getAny(properties.PolicyStatus) : undefined);
    ret.addResult('publicAccessBlockConfiguration', 'PublicAccessBlockConfiguration', properties.PublicAccessBlockConfiguration != null ? CfnAccessPointPublicAccessBlockConfigurationPropertyFromCloudFormation(properties.PublicAccessBlockConfiguration) : undefined);
    ret.addResult('vpcConfiguration', 'VpcConfiguration', properties.VpcConfiguration != null ? CfnAccessPointVpcConfigurationPropertyFromCloudFormation(properties.VpcConfiguration) : undefined);

    for (const [key, val] of Object.entries(cfn_parse.FromCloudFormation.omit(properties, 'Bucket', 'CreationDate', 'Name', 'NetworkOrigin', 'Policy', 'PolicyStatus', 'PublicAccessBlockConfiguration', 'VpcConfiguration')))  {
        ret.appendExtraProperty(key, val);
    }

    return ret;
}

That's a lot better already, no?

And it occurs to me that the for-loop at the end contains a duplicate list of properties that have already been set on the accumulation object. So in fact we could even simplify to this:

function CfnAccessPointPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult<CfnAccessPointProps> {
    properties = properties || {};
    const ret = new cfn_parse.FromCloudFormationPropertyObject<CfnAccessPointProps>();

    ret.addResult('bucket', 'Bucket', cfn_parse.FromCloudFormation.getString(properties.Bucket));
    ret.addResult('creationDate', 'CreationDate', properties.CreationDate != null ? cfn_parse.FromCloudFormation.getString(properties.CreationDate) : undefined);
    ret.addResult('name', 'Name', properties.Name != null ? cfn_parse.FromCloudFormation.getString(properties.Name) : undefined);
    ret.addResult('networkOrigin', 'NetworkOrigin', properties.NetworkOrigin != null ? cfn_parse.FromCloudFormation.getString(properties.NetworkOrigin) : undefined);
    ret.addResult('policy', 'Policy', properties.Policy != null ? cfn_parse.FromCloudFormation.getAny(properties.Policy) : undefined);
    ret.addResult('policyStatus', 'PolicyStatus', properties.PolicyStatus != null ? cfn_parse.FromCloudFormation.getAny(properties.PolicyStatus) : undefined);
    ret.addResult('publicAccessBlockConfiguration', 'PublicAccessBlockConfiguration', properties.PublicAccessBlockConfiguration != null ? CfnAccessPointPublicAccessBlockConfigurationPropertyFromCloudFormation(properties.PublicAccessBlockConfiguration) : undefined);
    ret.addResult('vpcConfiguration', 'VpcConfiguration', properties.VpcConfiguration != null ? CfnAccessPointVpcConfigurationPropertyFromCloudFormation(properties.VpcConfiguration) : undefined);

    ret.addUnknownPropertiesAsExtra(properties);

    return ret;
}

That also means we can get rid of the omit() helper, because that logic could just live inside FromCloudFormationPropertyObject (all it would do is need to hold a Set<> containing all keys it had already seen). Didn't do that yet because I wonder if it's going too far. But it sure deals with the ugliness some more.

What do you think?

Copy link
Contributor

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

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

Leaving whether or not to clean up the final omit loop up to you. Also if you want to rename any classes, or simplify some more, that's up to you.

I'm happy with this approach. Provisional approval.

@skinny85
Copy link
Contributor Author

skinny85 commented Dec 4, 2020

Thanks Rico. I've incorporated your comment about getting rid of omit(), by introducing the addUnknownPropertiesAsExtra() method (although I called it addUnrecognizedPropertiesAsExtra()).

Merging this in now!

@skinny85 skinny85 removed the pr/do-not-merge This PR should not be merged at this time. label Dec 4, 2020
@mergify
Copy link
Contributor

mergify bot commented Dec 4, 2020

Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 4dc7b34
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@mergify
Copy link
Contributor

mergify bot commented Dec 4, 2020

Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify mergify bot merged commit 45677ca into aws:master Dec 4, 2020
@skinny85 skinny85 deleted the feat/cfn-include-missing-props branch December 4, 2020 20:53
skiyani pushed a commit to skiyani/aws-cdk that referenced this pull request Dec 7, 2020
…he current CFN schema (aws#11822)

Currently, if cloudformation-include encounters a property in one of the resources the CDK has an L1 for
that is not in the version of the CloudFormation resource schema the module uses,
the property will be silently dropped. Of course, that is not ideal.

Make changes to our L1 code generation to preserve all properties that are not in the CFN schema.

Fixes aws#9717

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
flochaz pushed a commit to flochaz/aws-cdk that referenced this pull request Jan 5, 2021
…he current CFN schema (aws#11822)

Currently, if cloudformation-include encounters a property in one of the resources the CDK has an L1 for
that is not in the version of the CloudFormation resource schema the module uses,
the property will be silently dropped. Of course, that is not ideal.

Make changes to our L1 code generation to preserve all properties that are not in the CFN schema.

Fixes aws#9717

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/cloudformation-include Issues related to the "CFN include v.20" package contribution/core This is a PR that came from AWS. pr-linter/exempt-readme The PR linter will not require README changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[cfn-include] Make sure to either preserve or error out on properties not in the CFN schema
3 participants