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

Locals #802

Merged
merged 10 commits into from
Aug 1, 2019
Merged

Locals #802

merged 10 commits into from
Aug 1, 2019

Conversation

yorinasub17
Copy link
Contributor

@yorinasub17 yorinasub17 commented Jul 27, 2019

This implements the concept of locals in terragrunt config. This can be used to bind expressions to names for reusing within the config, so that you can DRY complex expressions within the config.

TODO:

  • Add more testing

@jfunnell
Copy link

Wow awesome, this is an excellent improvement if it works (might test it today..).

Like you said, I don't really see how this could have helped with issue #752 and that is fine.. I think the overall best solution to clean up some of the shortcomings of terragrunt is to implement the entire feature set in #759, which would probably require a hefty chunk of code/functionality to change.
In short, I think this is an acceptable stopgap for now. Just my 2c

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.

Nice addition!

README.md Outdated Show resolved Hide resolved
README.md Outdated
}
```

You can use any valid terragrunt expression in the `locals` configuration.
Copy link
Member

Choose a reason for hiding this comment

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

Prepare yourself for the "how do I reuse locals from another file?" question in 3... 2... 1... :-D

Perhaps tackle that head on and add a section on reusing/merging yaml files for now? And when we add get_input and get_output, we can replace it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok I can add that in.

FWIW, I was thinking about proposing a similar construct, but called globals which can be merged with the parent in an include call. I think the explicit separating of locals from globals is important, especially if and when include becomes more complex to allow inclusion of multiple levels of terragrunt config.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have any immediate plans to implement globals? We've just developed a need for something like that (having variables in our parent terragrunt.hcl that we can override from child terragrunt.hcls). While including a yaml file if it exists would work, it doesn't seem like the cleanest solution and we'd have to walk it back once there's a better way.

If you haven't started and you think it would be something that would be accepted, I don't mind creating a PR for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you haven't started and you think it would be something that would be accepted, I don't mind creating a PR for it.

I don't think we have immediate plans for globals. I would suggest first creating an issue (with Feature Request in the title) describing:

  • Your use case. Why do you need something like globals?
  • Ideal solution. Is globals what you want, or do you want something like get_local, which can take another terragrunt.hcl config file as input, a key and return the value assigned to that local in the target file?
  • Indicate whether you want to open a PR, either for the ideal solution or if that is challenging, a temporary workaround solution that works, but is better than what is available now.

config/config.go Outdated
// the config. For consistency, `locals` in the call to `decodeHcl` is always assumed to be nil.
func decodeAsTerragruntLocals(file *hcl.File, filename string, terragruntOptions *options.TerragruntOptions) (*terragruntLocals, error) {
terragruntLocals := terragruntLocals{}
err := decodeHcl(file, filename, &terragruntLocals, terragruntOptions, nil, nil)
Copy link
Member

Choose a reason for hiding this comment

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

What happens if a local references another local?

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 I didn't think about this possibility. I think currently it will panic or raise a syntax error, but it would be nice if it can be recursive. Let me do a quick spike to see how hard it is to implement this.

@yorinasub17
Copy link
Contributor Author

UPDATE:

I made a huge refactor of the implementation to convert the locals attribute to a block. This has the advantage of being able to reference other locals in the locals block, but at the expense of being unable to merge directly from yamldecode. I think this is an ok tradeoff considering that you can still import yamls as locals, only that it can't be merged at the top level (see the new docs I wrote).

Note that I am still optimizing the logs and error handling, so not quite ready to merge yet but I reached the point where it would be good to get some feedback on the new approach.

@yorinasub17
Copy link
Contributor Author

Ok @brikis98 this is ready for re-review given the new implementation.

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.

Nice!

}
```

##### Including globally defined locals
Copy link
Member

Choose a reason for hiding this comment

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

Thx for adding 👍

config/config.go Outdated
@@ -45,6 +47,15 @@ type terragruntConfigFile struct {
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
Skip *bool `hcl:"skip,attr"`
IamRole *string `hcl:"iam_role,attr"`

// This should be ignored
Copy link
Member

Choose a reason for hiding this comment

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

Why ignored?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This needs to be skipped here because hcl can't decode it correctly when it self references locals. It is not necessary to decode it into this struct here because it is evaluated in a separate loop.

However, we don't want to completely omit the existence of it because we want a syntax error if we have blocks that terragrunt doesn't expect (e.g typos). Hence it is added here as an attribute but the struct won't attempt to parse anything in it.

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps expand the comment a bit to explain this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

config/config.go Outdated
@@ -299,24 +318,37 @@ func ParseConfigString(configString string, terragruntOptions *options.Terragrun
// an include block)
func decodeAsTerragruntInclude(file *hcl.File, filename string, terragruntOptions *options.TerragruntOptions) (*terragruntInclude, error) {
terragruntInclude := terragruntInclude{}
err := decodeHcl(file, filename, &terragruntInclude, terragruntOptions, nil)
err := decodeHcl(file, filename, &terragruntInclude, terragruntOptions, nil, nil)
Copy link
Member

Choose a reason for hiding this comment

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

An include can't have its own locals?

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 point! Updated the evaluation order so it can be there.

config/locals.go Outdated
}
if localsBlock == nil {
// No locals block referenced in the file
terragruntOptions.Logger.Printf("Did not find any locals block: skipping evaluation.")
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to log this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented a crude version of log levels. This is actually quite useful to debug issues with locals and I expect we will need it once people start using it.

config/locals.go Outdated
return nil, nil
}

terragruntOptions.Logger.Printf("Found locals block: evaluating the expressions.")
Copy link
Member

Choose a reason for hiding this comment

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

Same here... Terragrunt doesn't currently support log levels, so the log statements here could be quite noisy...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above re:log levels.

config/locals.go Outdated
// further.
evaluatedLocals := map[string]cty.Value{}
evaluated := true
for len(locals) > 0 && evaluated {
Copy link
Member

Choose a reason for hiding this comment

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

Should we add a simple counter and ensure it stays under, say, 1000? Could be a nice safety valve in case of a bug in the code that leads to an infinite loop.

Actually, on that note, what if the code has:

locals {
  a = local.b
  b = local.a
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will fail because neither can be evaluated in the first loop, so evaluated will be false.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we add a simple counter and ensure it stays under, say, 1000? Could be a nice safety valve in case of a bug in the code that leads to an infinite loop.

Implemented.

// - the updated map of evaluated locals after this attempt
// - whether or not any locals were evaluated in this attempt
// - any errors from the evaluation
func attemptEvaluateLocals(
Copy link
Member

Choose a reason for hiding this comment

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

Yikes, this is quite complicated. I have some vague memory that the HCL parser has native support for partial evaluation specifically for this use case, but can't find it in the code base quickly...

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 searched but the only partial evaluation I could find was at the top file level, and not at the individual block level where we have expressions.

// Multiple references
x = 1
y = 2
z = local.x + local.y
Copy link
Member

Choose a reason for hiding this comment

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

Consider testing a few corner cases:

  • Reference to a local that isn't defined in a locals { ... } block
  • Reference to a local that isn't defined elsewhere in the code
  • Cyclical references
  • Several layers of references (a = local.b, b = local.c, c = local.d, etc)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a107c89

…blocks

- Expand comment on why locals evaluation is ignored at the file level
- Implement a crude version of Debugf that can be used for debug logging
- Add infinite loop protection
if os.Getenv("TG_LOG") == "DEBUG" {
logger.Printf(fmtString, fmtArgs...)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Ah, clever 👍

@yorinasub17
Copy link
Contributor Author

UPDATE a107c89 :

  • Added a bunch more test cases
  • Fixed a bug where we parsed multiple locals block, but silently ignored all but the first one defined. Now we throw an error if we detect multiple locals blocks.
  • Fixed a case where we weren't properly writing out diagnostics information.

With that, if the build passes, I think we are ready to merge + release!

@brikis98
Copy link
Member

LGTM!

@yorinasub17
Copy link
Contributor Author

Ok merging and releasing. Thanks for the review!

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.

4 participants