Skip to content
This repository has been archived by the owner on Apr 16, 2022. It is now read-only.

Fn::Select #104

Merged
merged 11 commits into from
Feb 15, 2018
Merged

Fn::Select #104

merged 11 commits into from
Feb 15, 2018

Conversation

kcculhwch
Copy link
Contributor

Hi, I'm just taking a stab at implementing Fn::Select here. I'm not sure if this is all in exactly the format you would like. Please let me know if this works, and what I can do to get the code up to par, if not.

Tests included should cover all major failure points, as well as a passing scenario.

Thanks,
David P. Smith
LinuxAcademy.com

Copy link
Owner

@martysweet martysweet left a comment

Choose a reason for hiding this comment

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

Hi David,

Thanks for your contribution to cfn-lint! I have made a few comments below, let me know if you need additional information or you disagree on anything.

I have used https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-select.html for the quotations.

Thanks,
Marty

src/validator.ts Outdated
}

let list = toGet[1]
if (!Array.isArray(list)) {
Copy link
Owner

Choose a reason for hiding this comment

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

It looks like CommaDelimitedList should be supported.

    Type: CommaDelimitedList
    Default: "10.0.48.0/24, 10.0.112.0/24, 10.0.176.0/24"

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-select.html

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 be sure to get that in there too.

Copy link
Owner

Choose a reason for hiding this comment

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

This should be automatically converted using this lookup https://github.com/martysweet/cfn-lint/blob/master/data/aws_parameter_types.json. So should just be a case of using the array.

There may be some validation issues here https://github.com/martysweet/cfn-lint/blob/master/src/validator.ts#L247-L263 which don't catch invalid CommaDelimitedList provided lists. If this turns into a lot of effort we can split improving that into another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, how would I go about getting that array? I'm not seeing it in workingInput, but maybe I'm looking in the wrong place. workingInput['Parameters'][parameterName]['Attributes']['Ref'] is just returning the original string. I could just do a .split(',') and .trim() each value in the array from that. But I feel like there is a better way to do that. :)

Copy link
Contributor Author

@kcculhwch kcculhwch Jan 30, 2018

Choose a reason for hiding this comment

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

Here's what I added to assignParametersOutput just after it checks allowedValues

        if(parameter['Type'] === "CommaDelimitedList" && typeof parameterValue === 'string') {
            parameterValue = parameterValue.split(',').map(x => x.trim());
            parameterValue.forEach(val => {
              if (val === ""){
                addError('crit', `Parameter ${parameterName} contains a CommaDelimitedList where the number of commas appears to be equal or greater than the list of items.`, ['Parameters', parameterName], "Parameters");
              }
            })
        }

on my local build, but I'm sure there is a better way somewhere

@@ -0,0 +1,245 @@
{
Copy link
Owner

Choose a reason for hiding this comment

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

Due to the size of these templates, it not particularly clear where the error specified in the test should be occurring. These would be nicer as smaller templates where the test case to fail is easy to spot, and relatively isolated. I.e. no parameters and only features which are required for the test to fail in the specified way.

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, I think I'll try to move my tests into something more a like a unit test. That will make it much more clear whats happening.

Copy link
Owner

Choose a reason for hiding this comment

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

These look much better!

src/validator.ts Outdated
if (index >= 0 && index < list.length) {
return list[index];
} else {
addError('warn', "First element of Fn::Select exceeds the length of the list, if list is a function, make sure it returns a longer list", placeInTemplate, "Fn::Select");
Copy link
Owner

Choose a reason for hiding this comment

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

This should probably result in a error instead as it will fail the deployment.

Fn::Select does not check for null values or if the index is out of bounds of the array. Both conditions will result in a stack error, so you should be certain that the index you choose is valid, and that the list contains non-null values.

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 was unsure on that one. Since it was possible that a mocked intrinsic could have supplied a shorter list than would be was expected. Fn::Select I think is recommended for choosing AZs, but it looked like the mocked getAZ was only returning three entries. (I'm not sure about that, I only had a minute to glance over it. - and I'm definitely not an expert that front :) )

Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, I was wondering about that too. It would involve lots of effort to improve the getAZ mock (i.e. integration with the AWS SDK), as you can't guarantee that all account have the same number of AZs in each region.

Therefore, your account might have a different number of available Availability Zones in a region than another account.

We could, however, have a flag to define the amount of AZs you wish to validate against, as we can always expect this to start from a, AFAIK.

If you can think of any other edge cases where making this an "error" would cause annoyance then we might need to think of another solution.

src/validator.ts Outdated
addError('crit', "Fn::Select requires the second element to resolve to a list, it appears to be a string", placeInTemplate, "Fn::Select");
return 'INVALID_SELECT';
}
if (!Array.isArray(list)) {
Copy link
Owner

Choose a reason for hiding this comment

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

As we are expecting an Array, can we check for the following property of null entries?

This list must not be null, nor can it have null entries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep.

expect(result).to.have.deep.property('templateValid', false);
expect(result['errors']['crit']).to.have.lengthOf(1);
expect(result['errors']['warn']).to.have.lengthOf(0);
});
Copy link
Owner

Choose a reason for hiding this comment

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

It would also be good to ensure the YAML shorthand is working as expected.

AvailabilityZone: !Select 
  - 0
  - !GetAZs 
    Ref: 'AWS::Region'

AvailabilityZone: !Select 
  - 0
  - Fn::GetAZs: !Ref 'AWS::Region'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep

src/validator.ts Outdated
if (!Array.isArray(list)) {
//we may need to resolve it
if (typeof list !== 'string') {
list = resolveIntrinsicFunction(list, Object.keys(list)[0]);
Copy link
Owner

Choose a reason for hiding this comment

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

There should be some validation around resolving an intrinsic function, this is to support nested functions, see https://github.com/martysweet/cfn-lint/blob/master/src/validator.ts#L894-L897 for an example. This file will need updating with the list of the supported functions from AWS. https://github.com/martysweet/cfn-lint/blob/master/data/aws_intrinsic_functions.json#L13

Supported Functions
For the Fn::Select index value, you can use the Ref and Fn::FindInMap functions.

For the Fn::Select list of objects, you can use the following functions:

Fn::FindInMap

Fn::GetAtt

Fn::GetAZs

Fn::If

Fn::Split

Ref

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for explaining that bit.

@kcculhwch
Copy link
Contributor Author

Thanks for the feedback! It may be a few days before I can address everything.

Thank you for making such a nice product!

Copy link
Owner

@martysweet martysweet left a comment

Choose a reason for hiding this comment

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

Looking good, is that all the functionality that is required? Just a couple of typos and formatting issues below.

src/validator.ts Outdated
@@ -526,7 +526,9 @@ function resolveIntrinsicFunction(ref: any, key: string) : string | boolean | st
case 'Fn::Not':
return doIntrinsicNot(ref, key);
case 'Fn::ImportValue':
return doIntrinsicImportValue(ref, key);
Copy link
Owner

Choose a reason for hiding this comment

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

Please, can you clean up the spacing issue on here?

src/validator.ts Outdated
} else if (typeof toGet[0] === 'number'){
index = toGet[0];
} else {
addError('crit', `Fn:Select's first arguement must be a number or resolve to a number, it appears to be a ${typeof(toGet[0])}`, placeInTemplate, "Fn::Select");
Copy link
Owner

Choose a reason for hiding this comment

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

arguement typo

src/validator.ts Outdated
return 'INVALID_SELECT';
}
} else if (list.indexOf(null) > -1) {
addError('crit', "FnSelect requires that the list be free of null values", placeInTemplate, "Fn::Select");
Copy link
Owner

Choose a reason for hiding this comment

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

FnSelect typo

src/validator.ts Outdated
if (index >= 0 && index < list.length) {
return list[index];
} else {
addError('crit', "First element of Fn::Select exceeds the length of the list, if list is a function, make sure it returns a longer list", placeInTemplate, "Fn::Select");
Copy link
Owner

Choose a reason for hiding this comment

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

I'm not too keen on the wording of this.

@@ -0,0 +1,245 @@
{
Copy link
Owner

Choose a reason for hiding this comment

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

These look much better!

…e to invalid json, to make sure it is invalid yaml too, as well as checking for null values in the yamlcleanup function
@@ -63,7 +63,7 @@ function cleanupYaml(ref: any){
let key = Object.keys(ref)[i];

// Resolve the function
if(ref[key].hasOwnProperty('class') && ref[key].hasOwnProperty('data')){
if( ref[key] !== null && ref[key].hasOwnProperty('class') && ref[key].hasOwnProperty('data')){
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 found that I needed to make a couple of changes to the parser to block the script from trying to parse out null entries in yaml arrays.

@@ -96,12 +96,12 @@ function cleanupYaml(ref: any){
ref[key] = {};
ref[key][outputKeyName] = outputData;

}else if(key != 'Attributes' && typeof ref[key] == "object"){
}else if(key != 'Attributes' && typeof ref[key] == "object" && ref[key] !== null){
Copy link
Contributor Author

Choose a reason for hiding this comment

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

And here too.

The result of this was that I had to modify the invalid json doc, so that it would also be invalid yaml

{'some': 'Invalid', 'json' => 0}
Copy link
Contributor Author

@kcculhwch kcculhwch Feb 5, 2018

Choose a reason for hiding this comment

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

I added the arrow assigment operator and 0 to make it both invalid json and invalid yaml

Copy link
Contributor Author

@kcculhwch kcculhwch left a comment

Choose a reason for hiding this comment

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

This should be most of it, but I want to do one more thorough combing through of the tests, to make sure I have them all generating the right error for the right reason.

@kcculhwch
Copy link
Contributor Author

Ok, did another once over on the tests, and made them check for specific responses.

@martysweet
Copy link
Owner

Great stuff @kcculhwch, thanks! Are you happy for this to be merged?

@kcculhwch
Copy link
Contributor Author

Yep, I think this is ready. I didn't update the README.md, yet, to indicate support for Fn::Select, as I figured that might need to wait until the release itself gets created.

@martysweet martysweet merged commit 581807d into martysweet:master Feb 15, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants