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

adding function to extract terraform vars from var file #733

Merged
merged 9 commits into from
Feb 15, 2021

Conversation

alpacamybags118
Copy link
Contributor

@alpacamybags118 alpacamybags118 commented Dec 9, 2020

This PR adds the following functionality to the terraform Options module:

  • Exposes a function that allows you to get a map of all the variables contained in each terraform VarFile specified in a terraform options object. This would allow you to reference these easily later on in a test for comparison, etc.

This is my first time contributing to this repo, so I tried to follow the style guides and everything to the best of my ability. Any feedback would be greatly appreciated!

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

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

Thanks for the PR!

A few thoughts:

  1. What is the use case you have for this? I'd like to hear concrete details to make sure we're adding support for the right thing.
  2. Following from the previous question, my guess is that it makes more sense to have a method to read a single value from a var file, rather than the whole var file. That way, you can parse it as a specific type (instead of interface{}), and use that value explicitly (whereas using the entire file seems unlikely). But again, this depends on the particular use case.
  3. Please see our error handling guidelines.
  4. Please make sure to run goimports.

@alpacamybags118
Copy link
Contributor Author

Thanks for the feedback @brikis98 !

As for the use case, I have found frequently that I've wanted to access a setting I've set in a tfvars file later in a test for use in comparison for assertion/validation. For example, I was changing a module that exposes an option to set a CNAME record. I was planning to use the value we set the CNAME record to for some validation. Since I couldn't access the value from the tfvars file, I had to end up taking it out and hard-code the value in the test and pass in both the var file and that individual var into the terraform options.

So, while this functionally isn't needed since you can get around cases where you do need to access certain variables, you would always need to hard-code them into your test and add additional code to import both a var and a var file in your terraform options. (unless I'm totally missing another way to do this).

Overall my argument is this functionality would allow you to not have to write additional code for use cases like this and keeps configuration more separated from the code. Hopefully what I'm saying makes sense 🤣

If you are OK with adding this, I'll go ahead and address the additional feedback you provided.

@brikis98
Copy link
Member

Thanks for the feedback @brikis98 !

As for the use case, I have found frequently that I've wanted to access a setting I've set in a tfvars file later in a test for use in comparison for assertion/validation. For example, I was changing a module that exposes an option to set a CNAME record. I was planning to use the value we set the CNAME record to for some validation. Since I couldn't access the value from the tfvars file, I had to end up taking it out and hard-code the value in the test and pass in both the var file and that individual var into the terraform options.

So, while this functionally isn't needed since you can get around cases where you do need to access certain variables, you would always need to hard-code them into your test and add additional code to import both a var and a var file in your terraform options. (unless I'm totally missing another way to do this).

Overall my argument is this functionality would allow you to not have to write additional code for use cases like this and keeps configuration more separated from the code. Hopefully what I'm saying makes sense 🤣

If you are OK with adding this, I'll go ahead and address the additional feedback you provided.

Makes sense to me!

@alpacamybags118
Copy link
Contributor Author

Crap, pushed too soon lol. Don't review yet - still need to update/add tests.

@alpacamybags118
Copy link
Contributor Author

@brikis98 happy (late) new year! I finally got a chance to come back to refactoring this PR a bit. I decided to structure it more like the output module and provide different functions for getting input file values as their different types (string, map, list), along with adding some new error types.

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

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

Thanks! Left some comments/thoughts on the API.

modules/terraform/errors.go Outdated Show resolved Hide resolved
type InputFileKeyNotFound string

func (err InputFileKeyNotFound) Error() string {
return fmt.Sprintf("tfvar file doesn't contain a value for the key %q", string(err))
Copy link
Member

Choose a reason for hiding this comment

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

NIT: Could the error message include the path to the tfvar file?

return nil, err
}

_, exists := variables[key]
Copy link
Member

Choose a reason for hiding this comment

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

NIT: any reason not to store the returned value here instead of looking up variables[key] multiple times below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, will update.

}

if reflect.TypeOf(variables[key]).String() != "[]map[string]interface {}" {
return nil, UnexpectedOutputType{Key: key, ExpectedType: "map", ActualType: reflect.TypeOf(variables[key]).String()}
Copy link
Member

Choose a reason for hiding this comment

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

ExpectedType is misleading here, as you're actually looking for []map[string]interface {}...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good catch! Probably a bad copy-paste :P

return nil, UnexpectedOutputType{Key: key, ExpectedType: "map", ActualType: reflect.TypeOf(variables[key]).String()}
}

mapKeys := variables[key].([]map[string]interface{})[0]
Copy link
Member

Choose a reason for hiding this comment

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

Check that the slice is not empty before looking up index 0 to avoid a potential panic.

return fmt.Sprintf("%v", variable), nil
}

// GetVariableAsMapFromVarFile Gets the map represention of a variable from a provided input file found in VarFile
Copy link
Member

Choose a reason for hiding this comment

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

NIT: in both descriptions, it's probably worth calling out that this is specifically a map of strings. For maps with more complicated nested types, or other types in general, use GetAllVariablesFromVarFile.

return resultMap, nil
}

// GetVariableAsListFromVarFile Gets the string list represention of a variable from a provided input file found in VarFile
Copy link
Member

Choose a reason for hiding this comment

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

Same note here that this only works for lists of strings and to use GetAllVariablesFromVarFile for more complicated types.

return nil, err
}

if _, exists := variables[key]; !exists {
Copy link
Member

Choose a reason for hiding this comment

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

Same NIT here about storing the return value instead of looking it up again several times below.

}

// GetAllVariablesFromVarFile Parses all data from a provided input file found in VarFile and returns them as a key-value map
func GetAllVariablesFromVarFile(t *testing.T, option *Options, fileName string) map[string]interface{} {
Copy link
Member

Choose a reason for hiding this comment

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

A map[string]interface{} is a bit unwiedly and error prone to use, as you have to cast all the values within the map. What are your thoughts instead on an interface like:

func GetAllVariablesFromVarFile(t *testing.T, option *Options, fileName string, returnValue interface{})

Where returnValue is a pointer the user can pass in with any custom struct, and we will parse the tfvar file into that struct, including all the types within it. This is similar to OutputStruct.

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 valid. It would certainly make it easier for the user. I did see to minimize using pointers in the best practices for this repo and this is a bit unique compared to the other functions, but it probably makes sense given the complexity of the return type.


// GetAllVariablesFromVarFileE Parses all data from a provided input file found ind in VarFile and returns them as a key-value map
// Retursn an error if the specified file does not exist, the specified file is not readable, or the specified file cannot be decoded from HCL
func GetAllVariablesFromVarFileE(t *testing.T, option *Options, fileName string) (map[string]interface{}, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Why take in a fileName and option struct, and have to search for the file within it, rather than take in solely the path directly to a var file? This applies to all the methods in this 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.

That makes sense. I think I wanted to use the options struct since I was originally thinking in just the scope of what is provided to options, but its just adding more unneeded logic. I'll adjust this.

@alpacamybags118
Copy link
Contributor Author

Thank you for the feedback! I'll get to making the updates later this week.

@alpacamybags118
Copy link
Contributor Author

@brikis98 I addressed all your feedback! I do have one thought, though, that I wanted to run by you. With these changes, it seems like maybe this code doesn't quite fit into the options file since it no longer interfaces with the options object. Maybe should be separated into its own module, like input? What are your thoughts?

@brikis98
Copy link
Member

brikis98 commented Feb 1, 2021

@brikis98 I addressed all your feedback! I do have one thought, though, that I wanted to run by you. With these changes, it seems like maybe this code doesn't quite fit into the options file since it no longer interfaces with the options object. Maybe should be separated into its own module, like input? What are your thoughts?

Sorry for the delay. Agreed it doesn't quite fit into options.go. Perhaps var-files.go or similar instead?

@alpacamybags118
Copy link
Contributor Author

Updated to move the code to a different file!

@brikis98
Copy link
Member

brikis98 commented Feb 8, 2021

Great, thanks! Would you mind rebasing on master? I can then kick off tests.

@alpacamybags118
Copy link
Contributor Author

Done! @brikis98

go.mod Outdated Show resolved Hide resolved
Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

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

Apologies, found two more NITs, and then we're good!

Comment on lines 202 to 204
if err == nil {
t.FailNow()
}
Copy link
Member

Choose a reason for hiding this comment

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

NIT: use require.Error instead.

modules/terraform/var-file_test.go Outdated Show resolved Hide resolved
@alpacamybags118
Copy link
Contributor Author

Updated!

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

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

LGTM! I'll kick off tests now.

@alpacamybags118
Copy link
Contributor Author

Hm, looks like a test not related to my code changes failed TestTerraformAwsRdsExample - test . It does look like my tests passed though:

=== RUN   TestGetVariablesFromVarFilesAsString
--- PASS: TestGetVariablesFromVarFilesAsString (0.00s)
=== RUN   TestGetVariablesFromVarFilesAsStringKeyDoesNotExist
--- PASS: TestGetVariablesFromVarFilesAsStringKeyDoesNotExist (0.00s)
=== RUN   TestGetVariableAsMapFromVarFile
--- PASS: TestGetVariableAsMapFromVarFile (0.00s)
=== RUN   TestGetVariableAsMapFromVarFileNotMap
--- PASS: TestGetVariableAsMapFromVarFileNotMap (0.00s)
=== RUN   TestGetVariableAsMapFromVarFileKeyDoesNotExist
--- PASS: TestGetVariableAsMapFromVarFileKeyDoesNotExist (0.00s)
=== RUN   TestGetVariableAsListFromVarFile
--- PASS: TestGetVariableAsListFromVarFile (0.00s)
=== RUN   TestGetVariableAsListNotList
--- PASS: TestGetVariableAsListNotList (0.00s)
=== RUN   TestGetVariableAsListKeyDoesNotExist
--- PASS: TestGetVariableAsListKeyDoesNotExist (0.00s)
=== RUN   TestGetAllVariablesFromVarFileEFileDoesNotExist
--- PASS: TestGetAllVariablesFromVarFileEFileDoesNotExist (0.00s)
=== RUN   TestGetAllVariablesFromVarFileBadFile
--- PASS: TestGetAllVariablesFromVarFileBadFile (0.00s)
=== RUN   TestGetAllVariablesFromVarFile
--- PASS: TestGetAllVariablesFromVarFile (0.00s)

@alpacamybags118
Copy link
Contributor Author

Ah, looks like it might be using an instance that isn't valid? 2021-02-15T15:38:55Z logger.go:66: Error: Error creating DB Instance: InvalidParameterValue: Invalid DB Instance class: db.t2.micro . I thought that was a valid one? https://aws.amazon.com/rds/instance-types/

@brikis98
Copy link
Member

Yup, it's an unrelated intermittent issue in some AWS regions: #783. All other tests passed, so I'm going to merge, and we'll deal with that issue later. Thanks!

@brikis98 brikis98 merged commit 88ba3f0 into gruntwork-io:master Feb 15, 2021
@brikis98
Copy link
Member

@WafflesMcDuff
Copy link

#784

This was referenced Mar 11, 2021
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.

3 participants