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

Add support for cf and codepipeline parameters file to cf deploy #5443

Merged
merged 4 commits into from Aug 10, 2020

Conversation

vz10
Copy link
Contributor

@vz10 vz10 commented Aug 4, 2020

Add support for cf and codepipeline parameters file to cf deploy command

Issue #, if available: #2828

Description of changes:

Current state:

The current format for “—parameter-overrides“ option is

ParameterKey1=ParameterValue1 ParameterKey2=ParameterKey2

or JSON format

  [
    "ParameterKey1=ParameterValue1",
    "ParameterKey2=ParameterKey2"
  ]

Proposed improvements:

Keep the shorthand format without changing but update the file parser to accept data in “CloudFromation format” and “CodePipeline format”, it means such JSON formats will be valid as well

  • “CloudFromation format”
[
   {
        "ParameterKey": "ParameterKey1",
        "ParameterValue": "ParameterValue1",
    },
    {
        "ParameterKey": "ParameterKey2",
        "ParameterValue": "ParameterValue2",
    },
...
]
  • “CodePipeline format”
[
    "Parameters": {
        "ParameterKey1": "ParameterValue1",
        "ParameterKey2": "ParameterValue2"`
    }

...
]

***Note: *the logic of the “—parameter-overrides“ option won’t be changed, it means that it will expect only parameter’s keys and values and throw ValidationError if get unexpected keys (e. g. UsePreviousValue or **ResolvedValue **which exist in CloudFormation format).. If parameter not set will be used previous value if stack is updated or default value for stack creation.

Examples

Following command deploys template named template.json to a stack named my-new-stack:

aws cloudformation deploy --template-file /path_to_template/template.json --stack-name my-new-stack --parameter-overrides BucketName=MyBucket Tag1value=awesomeBucket
template.json{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "AWS CloudFormation Sample S3 bucket Template.",
  "Parameters": {
    "BucketName": {
      "Description": "Name of s3 Bucket",
      "Type": "String"
    },
    "Tag1value": {
      "Description": "Tag1 value",
      "Default": "value1",
      "Type": "String"
    },
    "Tag2value": {
      "Description": "Tag2 value",
      "Default": "value2",
      "Type": "String"
    }
  },
  "Resources": {
    "s3bucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": {"Ref": "BucketName"},
        "Tags": [
          {
            "Key": "Tag1",
            "Value": {"Ref": "Tag1value"}
          },
          {
            "Key": "Tag2",
            "Value": {"Ref": "Tag2value"}
          }
        ]
      }
    }
  }
}

You can do the same with inline json

aws cloudformation deploy --template-file /path_to_template/template.json --stack-name my-new-stack --parameter-overrides '["BucketName=MyBucket", "Tag1value=awesomeBucket"]'

Or you can do the same using JSON file

aws cloudformation deploy --template-file /path_to_template/template.json --stack-name my-new-stack --parameter-overrides file://parameter.json 

parameter.json in CloudFormation format
[
   {
        "ParameterKey": "BucketName",
        "ParameterValue": "MyBucket",
    },
    {
        "ParameterKey": "Tag1value",
        "ParameterValue": "awesomeBucket",
    },
]

parameter.json in CodePipeline format
[
    "Parameters": {
        "BucketName": "MyBucket",
        "Tag1value": "awesomeBucket"
    }
]

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@codecov-commenter
Copy link

codecov-commenter commented Aug 4, 2020

Codecov Report

Merging #5443 into v2 will increase coverage by 0.00%.
The diff coverage is 96.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##               v2    #5443   +/-   ##
=======================================
  Coverage   94.47%   94.48%           
=======================================
  Files         234      234           
  Lines       17838    17887   +49     
=======================================
+ Hits        16853    16900   +47     
- Misses        985      987    +2     
Impacted Files Coverage Δ
awscli/customizations/cloudformation/deploy.py 98.42% <96.00%> (-1.58%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b7d1487...4d56f51. Read the comment docs.

Copy link
Member

@kyleknap kyleknap left a comment

Choose a reason for hiding this comment

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

Looks like a good start. Just had some suggestions to build on it.

Comment on lines +407 to +411
"""
Tests that we can parse parameter arguments from file in
CloudFormation parameters file format
:return:
"""
Copy link
Member

Choose a reason for hiding this comment

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

I think the cloudformation package/deploy commands are a bit of an outlier, but typically we avoid putting docstrings on the test cases. The name of the test, input, and expected output should speak for themselves.

self.PARAMETER_OVERRIDE_CMD
)
return result
else:
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if we should allow users to be able to send these new formats through the command line as JSON? For example, this should probably work:

aws cloudformation deploy \
    --stack-name test-cfn \
    --template-file template.json \
    --parameter-overrides "[{\"ParameterKey\":\"Tag1value\",\"ParameterValue\":\"awesomeBucket\"}]"

Generally, the expectation for CLI usage is that if it can be specified as a file, it can also be specified directly to the command line. While I expect most people to be using the file, I do not think we should be limiting the ability to pass the new formats in directly to the command line as users may be confused by this inconsistency and they may not necessarily want to write out a file in order to run a command.

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'm not sure anyone will do it but to be consistent we need to allow it, I think

'type': 'string'
}
},
'nargs': '+',
'default': [],
'help_text': (
'A list of parameter structures that specify input parameters'
Copy link
Member

Choose a reason for hiding this comment

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

Just a reminder we need doc updates for these new formats.

'type': 'string'
}
},
'nargs': '+',
'default': [],
'help_text': (
'A list of parameter structures that specify input parameters'
Copy link
Member

Choose a reason for hiding this comment

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

We also should add a changelog entry for this.

output = {"Key1": "Value1",
"Key2": '[1,2,3]',
"Key3": '{"a":"val", "b": 2}'}
result = self.deploy_command.parse_parameter_overrides(data)
Copy link
Member

Choose a reason for hiding this comment

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

The unit tests are fine here, but I think we should probably have extensive functional tests here to make sure that this parsing logic syncs up with how the values are initially parsed by the main CLI parser. Specifically, I think we should have all of the following cases (assuming we accept in-line JSON for the new formats):

  • Short-hand syntax for original --parameter-overrides
  • In-line JSON for original --parameter-overrides
  • File-based syntax for original --parameter-overrides
  • In-line JSON for “CloudFromation format”
  • File-based JSON for “CloudFromation format”
  • Validation of invalid “CloudFromation format”
  • In-line JSON for “CodePipeline format”
  • File-based JSON for “CodePipeline format”
  • Validate against just generally invalid JSON documents that do not follow any of the three valid schemas.

I realize the cloudformation deploy commands do not have great functional tests, but this will help ensure we do not introduce any regressions now or in the future for the various options we support.

# }
if isinstance(data, dict):
return data.get('Parameters', None)

Copy link
Member

Choose a reason for hiding this comment

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

Left this off of my last review... Should we validate that it does not have any of the top-level keys like Tags and StackPolicy? I think it makes sense because the CLI is generally strict on making sure all parameters provided will be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's discussible, if we take only Parameters key and just ignore the rest it'll allow customers to reuse the same files without charging

Copy link
Member

Choose a reason for hiding this comment

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

Yeah that's fair given this change is more of about expanding compatibility.

@vz10 vz10 force-pushed the cf_deploy_parameters_file branch from 8175c75 to 889219a Compare August 7, 2020 00:37
@vz10 vz10 force-pushed the cf_deploy_parameters_file branch from 889219a to 1d928af Compare August 7, 2020 00:43
Copy link
Member

@kyleknap kyleknap left a comment

Choose a reason for hiding this comment

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

Nice. It's looking good. I just had a few more suggestions.

@@ -0,0 +1,5 @@
{
"type": "feature",
Copy link
Member

Choose a reason for hiding this comment

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

Let's change this to an enhancement. Typically, feature means we will do a minor version bump and I'm not sure if this update warrants one.

{
"type": "feature",
"category": "``cloudformation``",
"description": "CloudFormation ``deploy`` command now supports various JSON file formats as an input for ``--parameter-overrides`` option."
Copy link
Member

Choose a reason for hiding this comment

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

We should probably link the GitHub issue that it fixes in the description.

Comment on lines +68 to +78
template = '''{
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": {
"Key1": {
"Type": "String"
},
"Key2": {
"Type": "String"
}
}
}'''
Copy link
Member

Choose a reason for hiding this comment

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

For any time we are writing a JSON document out to a file, it may make sense to expose a create_json_file method on the test class that accepts the content of the json document as a dictionary. So for example here:

template = {
     "AWSTemplateFormatVersion": "2010-09-09",
     "Parameters": {
         "Key1": {
            "Type": "String"
         },
         "Key2": {
            "Type": "String"
         }
     }
}
path = self.create_json_file('template.json', template)
...

Specifically when the template or parameters is in the form of python dictionary it is a bit easier to work with (e.g. you don't have to worry about getting JSON syntax correctly) and expand on it (e.g you want to add or remove keys from some default JSON document).

},
{
"ParameterKey": "Key2",
"ParameterValue": "Value2",
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if this is valid JSON with the trailing commas.

# }
if isinstance(data, dict):
return data.get('Parameters', None)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah that's fair given this change is more of about expanding compatibility.

Comment on lines 418 to 423
if result is None:
return self.parse_key_value_arg(
data,
self.PARAMETER_OVERRIDE_CMD
)
return result
Copy link
Member

Choose a reason for hiding this comment

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

So I'm not entire sure we should be relying on the old parsing behavior as the final attempt and not provide any helpful error message after it. Specifically playing around with this I got some really weird looking errors.

In this example, I accidentally misspelled ParameterValue as ParameterVlue

$ aws cloudformation deploy --stack-name test-cfn --template-file template.json --parameter-overrides "[{\"ParameterKey\":\"BucketName\",\"ParameterVlue\":\"kyleknap-test-cfn-bucket\"},{\"ParameterKey\":\"Tag1value\",\"ParameterValue\":\"awesomeBucket\"}]"

'dict' object has no attribute 'split'

Then even in the test case for just a generally bad structure, this message is confusing:

$ aws cloudformation deploy --stack-name test-cfn --template-file template.json --parameter-overrides '{"SomeKey":[{"RedundantKey": "RedundantValue"}]}'

['SomeKey'] value passed to --parameter-overrides must be of format Key=Value

In general, it might make sense to abstract this logic a bit into classes/this format:

class BaseParameterOverrideParser:
     def can_parse(self, data):
           # Returns true/false if it can parse

    def parse(self, data):
         # Return the properly formatted parameter dictionary

# Example parser
class CodePipelineParameterOverrideParser(BaseParameterOverrideParser):
     def can_parse(self, data):
         return isinstance(data, dict) and 'Parameters' in data

     def parse(self, data):
         return data['Parameters']


# In the DeployCommand class
def parse_parameter_overrides(self, arg_value):
     # After checking if it is json:
     parsers = [
          CfnParameterOverrideParser(),  # maybe find better names here
          CodePipelineParameterOverrideParser(),
          StringEqualsParameterOverrideParser()  
     ]
     for parser in parsers:
         if parser.can_parse(data):
            return parser.parse(data)
     raise InvalidParameterOverrideFormat('Show helpful information about the accepted formats')

The advantages with this approach is that:

  • We can avoid weird/unexpected errors from trying to parse a format that will not work.
  • We can throw a more helpful error about generally incorrect parameter formats instead of relying on any one specific parser errors.

},
]

Note: Only ParameterKey and ParameterValue are expected keys, command will through an exception if receives unexpected keys (e.g. UsePreviousValue or ResolvedValue).
Copy link
Member

Choose a reason for hiding this comment

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

Another thing I noticed when I generated the docs we should use the reStructuredText note e.g. (.. note::) so that this wording does not get captured in the codeblock.

Copy link
Member

@kyleknap kyleknap left a comment

Choose a reason for hiding this comment

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

Awesome. Just had a few really small comments and we should be good to merge this.

self.command += ' --parameter-overrides file://%s' % path
self.run_cmd(self.command)
self._assert_parameters_parsed()

def test_parameter_overrides_from_invalid_cf_like_json_file(self):
Copy link
Member

Choose a reason for hiding this comment

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

We should probably keep this test but just make sure we assert that the new error message is coming back.

if parser.can_parse(data):
return parser.parse(data)
raise ParamValidationError(
'JSON passed to --parameter-overrides must be in of '
Copy link
Member

Choose a reason for hiding this comment

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

Instead of must be in of let's change it to must be one of

Comment on lines +427 to +429
if isinstance(arg_value, str):
return json.loads(arg_value)
Copy link
Member

Choose a reason for hiding this comment

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

Then is this the case where it got loaded from a file? If so, it might be good to include that in the comment.


.. note::

Only ParameterKey and ParameterValue are expected keys, command will through an exception if receives unexpected keys (e.g. UsePreviousValue or ResolvedValue).
Copy link
Member

Choose a reason for hiding this comment

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

Typo: through -> throw

@vz10 vz10 force-pushed the cf_deploy_parameters_file branch from 56cb38a to 4d56f51 Compare August 7, 2020 23:57
Copy link
Member

@kyleknap kyleknap left a comment

Choose a reason for hiding this comment

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

Looks good! 🚢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants