Repository: aws-cloudformation/rain
Pull request #78 (2021-10-25 16:19:21)

Add cli config flag
*Issue #11 

*Description of changes:* Adds support for  parameters/tags as a json file for the deploy command. See the issue for more details on why this may be useful.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

gouglhupf: @stilvoid Ping :). Would it be possible to have a look at these changes and post your thoughts?
stilvoid: Hey, thanks so much for this and apologies for the delay in getting back to you!

I like the approach and this definitely fits what I had in mind but I have a few minor changes I'd suggest which I'll put into a review on this PR in a moment.

Patch:
diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go
index 4bfce67a..1c34b906 100644
--- a/internal/cmd/deploy/deploy.go
+++ b/internal/cmd/deploy/deploy.go
@@ -3,6 +3,7 @@ package deploy
 import (
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"path/filepath"
 	"regexp"
 
@@ -12,15 +13,22 @@ import (
 	"github.com/aws-cloudformation/rain/internal/console"
 	"github.com/aws-cloudformation/rain/internal/console/spinner"
 	"github.com/aws-cloudformation/rain/internal/ui"
+	"gopkg.in/yaml.v3"
 
 	"github.com/aws/smithy-go/ptr"
 	"github.com/spf13/cobra"
 )
 
+type configFileFormat struct {
+    Parameters map[string]string `yaml:"Parameters"`
+    Tags map[string]string `yaml:"Tags"`
+}
+
 var detach bool
 var yes bool
 var params []string
 var tags []string
+var configFilePath string
 var terminationProtection bool
 var keep bool
 
@@ -33,7 +41,32 @@ If you don't specify a stack name, rain will use the template filename minus its
 
 If a template needs to be packaged before it can be deployed, rain will package the template first.
 Rain will attempt to create an S3 bucket to store artifacts that it packages and deploys.
-The bucket's name will be of the format rain-artifacts-<AWS account id>-<AWS region>`,
+The bucket's name will be of the format rain-artifacts-<AWS account id>-<AWS region>.
+
+The config flag can be used to programmatically set tags and parameters.
+The format is similar to the "Template configuration file" for AWS CodePipeline just without the
+'StackPolicy' key. The file can be in YAML or JSON format.
+
+JSON:
+{
+  "Parameters" : {
+    "NameOfTemplateParameter" : "ValueOfParameter",
+    ...
+  },
+  "Tags" : {
+    "TagKey" : "TagValue",
+    ...
+  }
+}
+
+YAML:
+Parameters:
+  NameOfTemplateParameter: ValueOfParameter
+  ...
+Tags:
+  TagKey: TagValue
+  ...
+`,
 	Args:                  cobra.RangeArgs(1, 2),
 	DisableFlagsInUseLine: true,
 	Run: func(cmd *cobra.Command, args []string) {
@@ -56,10 +89,46 @@ The bucket's name will be of the format rain-artifacts-<AWS account id>-<AWS reg
 		}
 
 		// Parse tags
-		parsedTags := ListToMap("tag", tags)
+		parsedTagFlag := ListToMap("tag", tags)
 
 		// Parse params
-		parsedParams := ListToMap("param", params)
+		parsedParamFlag := ListToMap("param", params)
+
+		var combinedTags map[string]string
+		var combinedParameters map[string]string
+
+		if len(configFilePath) != 0 {
+			configFileContent, err := ioutil.ReadFile(configFilePath)
+			if err != nil {
+				panic(ui.Errorf(err, "unable to read config file '%s'", configFilePath))
+			}
+
+			var configFile configFileFormat
+			err = yaml.Unmarshal([]byte(configFileContent), &configFile)
+			if err != nil {
+				panic(ui.Errorf(err, "unable to parse yaml in '%s'", configFilePath))
+			}
+
+			combinedTags = configFile.Tags
+			combinedParameters = configFile.Parameters
+
+			for k, v := range parsedTagFlag {
+				if _, ok := combinedTags[k]; ok {
+					fmt.Println(console.Yellow(fmt.Sprintf("tags flag overrides tag in config file: %s", k)))
+				}
+				combinedTags[k] = v
+			}
+	
+			for k, v := range parsedParamFlag {
+				if _, ok := combinedParameters[k]; ok {
+					fmt.Println(console.Yellow(fmt.Sprintf("params flag overrides parameter in config file: %s", k)))
+				}
+				combinedParameters[k] = v
+			}
+		} else {
+			combinedTags = parsedTagFlag
+			combinedParameters = parsedParamFlag
+		}
 
 		// Package template
 		spinner.Push(fmt.Sprintf("Preparing template '%s'", base))
@@ -73,7 +142,7 @@ The bucket's name will be of the format rain-artifacts-<AWS account id>-<AWS reg
 
 		// Parse params
 		config.Debugf("Handling parameters")
-		parameters := getParameters(template, parsedParams, stack.Parameters, stackExists)
+		parameters := getParameters(template, combinedParameters, stack.Parameters, stackExists)
 
 		if config.Debug {
 			for _, param := range parameters {
@@ -87,7 +156,7 @@ The bucket's name will be of the format rain-artifacts-<AWS account id>-<AWS reg
 
 		// Create change set
 		spinner.Push("Creating change set")
-		changeSetName, createErr := cfn.CreateChangeSet(template, parameters, parsedTags, stackName)
+		changeSetName, createErr := cfn.CreateChangeSet(template, parameters, combinedTags, stackName)
 		if createErr != nil {
 			panic(ui.Errorf(createErr, "error creating changeset"))
 		}
@@ -171,6 +240,7 @@ func init() {
 	Cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Don't ask questions; just deploy.")
 	Cmd.Flags().StringSliceVar(&tags, "tags", []string{}, "Add tags to the stack. Use the format key1=value1,key2=value2.")
 	Cmd.Flags().StringSliceVar(&params, "params", []string{}, "Set parameter values. Use the format key1=value1,key2=value2.")
+	Cmd.Flags().StringVarP(&configFilePath, "config", "c", "", "YAML or JSON file to set tags and parameters.")
 	Cmd.Flags().BoolVarP(&terminationProtection, "termination-protection", "t", false, "Enable  termination protection on the stack.")
 	Cmd.Flags().BoolVarP(&keep, "keep", "k", false, "Keep deployed resources after a failure by disabling rollbacks.")
 }


Test patch:
diff --git a/internal/cmd/deploy/deploy_test.go b/internal/cmd/deploy/deploy_test.go
index e5af865b..4bb94752 100644
--- a/internal/cmd/deploy/deploy_test.go
+++ b/internal/cmd/deploy/deploy_test.go
@@ -19,12 +19,37 @@ func Example_deploy_help() {
 	//
 	// If a template needs to be packaged before it can be deployed, rain will package the template first.
 	// Rain will attempt to create an S3 bucket to store artifacts that it packages and deploys.
-	// The bucket's name will be of the format rain-artifacts-<AWS account id>-<AWS region>
+	// The bucket's name will be of the format rain-artifacts-<AWS account id>-<AWS region>.
+	//
+	// The config flag can be used to programmatically set tags and parameters.
+	// The format is similar to the "Template configuration file" for AWS CodePipeline just without the
+	// 'StackPolicy' key. The file can be in YAML or JSON format.
+	//
+	// JSON:
+	// {
+	//   "Parameters" : {
+	//     "NameOfTemplateParameter" : "ValueOfParameter",
+	//     ...
+	//   },
+	//   "Tags" : {
+	//     "TagKey" : "TagValue",
+	//     ...
+	//   }
+	// }
+	//
+	// YAML:
+	// Parameters:
+	//   NameOfTemplateParameter: ValueOfParameter
+	//   ...
+	// Tags:
+	//   TagKey: TagValue
+	//   ...
 	//
 	// Usage:
 	//   deploy <template> [stack]
 	//
 	// Flags:
+	//   -c, --config string            YAML or JSON file to set tags and parameters.
 	//   -d, --detach                   Once deployment has started, don't wait around for it to finish.
 	//   -h, --help                     help for deploy
 	//   -k, --keep                     Keep deployed resources after a failure by disabling rollbacks.