-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
fix(terraform): hcl object expressions to return references #8271
base: main
Are you sure you want to change the base?
fix(terraform): hcl object expressions to return references #8271
Conversation
References included in hcl objects should be returned from 'Attribute.AllReferences()'. Traverse the object fields recursively to locate references.
pkg/iac/terraform/attribute.go
Outdated
@@ -971,6 +971,10 @@ func (a *Attribute) AllReferences(blocks ...*Block) []*Reference { | |||
func (a *Attribute) referencesFromExpression(expression hcl.Expression) []*Reference { | |||
var refs []*Reference | |||
switch t := expression.(type) { | |||
case *hclsyntax.ObjectConsExpr: | |||
for _, item := range t.Items { | |||
refs = append(refs, a.referencesFromExpression(item.ValueExpr)...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to use createDotReferenceFromTraversal
, not referencesFromExpression
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modified a unit test.
This is an object that sets foo
to a conditional expression. That conditional expression has 2 references in it.
{foo = 1 == 1 ? var.bar : data.aws_ami.ubuntu.most_recent}
Maybe I am misunderstanding, but if I do
if ref, err := createDotReferenceFromTraversal(a.module, item.KeyExpr.Variables()...); err == nil {
refs = append(refs, ref)
}
// And do the same for the ValueExpression
I get variable.bar.data.aws_ami.ubuntu.most_recent
. Since Variables()
call just appends all traversal types:
https://github.com/hashicorp/hcl/blob/main/hclsyntax/variables.go#L18-L20
It is not intelligent to know the conditional expression has 2 different references, and just returns them as a single list. Rather than 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nikpivkin let me know I am misusing createDotReferenceFromTraversal
Hi @Emyrk! Thanks for contributing! Left a couple of comments. Also can you fix the linting issues? |
pkg/iac/terraform/attribute.go
Outdated
case *hclsyntax.ParenthesesExpr: | ||
refs = append(refs, a.referencesFromExpression(t.Expression)...) | ||
case *hclsyntax.ObjectConsKeyExpr: | ||
refs = append(refs, a.referencesFromExpression(t.Wrapped)...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using your example (local.foo): local.foo
, I also had to add these @nikpivkin
@nikpivkin unrelated, but is there a reason the underlying I was wondering if a getter like |
What's your use case for this? |
I am doing work on https://github.com/coder/coder. Terraform is the configuration language we use for the underlying developer workspaces. To give even more functionality/flexibility in the experience, we have a feature called "Build Parameters". See example image. In order to speed up some of the inner loop and provide more immediate feedback on these parameters, I am seeking to use static anaylsis of the terraform (using your package, which is awesome by the way ❤️). Now this is not perfect, if the parameters include a reference to an external data source ( I intend to use the existing terraform state to populate these unknown references in some second "pass/step". I do not see this package injesting a tfstate, so was going to build something to mutate the eval context. My usage of the specific functionality is data "coder_workspace_tags" "custom_workspace_tags" {
tags = {
// AllReferences() = [docker_image.ubuntu.repo_digest, docker_image.centos]
"foo" = docker_image.ubuntu.repo_digest
"bar" = docker_image.centos.repo_digest
"qux" = "quux"
}
} I am using So I can tell the reference To do this, I would either need to have the underlying I see additional value in exposing the |
Thanks for the explanation. I don't see anything inherently wrong with exposing it. WDYT @nikpivkin? |
@Emyrk It seems we can just use the |
I think we can provide a method to get the |
@nikpivkin feel free to make changes to this PR directly 👍. It is in my fork, so you might have to make a branch in this repo. Or I can give you perms in my fork. I have copied some of your code to take the
Another benefit of exposing is I can create and decorate An example of some code I have that expects an attribute to have a given type: func (a *expectedAttribute) expectedTypeError(attr *terraform.Attribute, expectedType string) {
a.error(hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid attribute type",
Detail: fmt.Sprintf("The attribute %q must be of type %q, found type %q", attr.Name(), expectedType, attr.Type().FriendlyNameForConstraint()),
Subject: &attr.HCLAttribute().Range,
Context: &a.p.block.HCLBlock().DefRange,
Expression: attr.HCLAttribute().Expr,
EvalContext: a.p.block.Context().Inner(),
})
} tagsAttr := block.GetAttribute("tags")
if tagsAttr.IsNil() {
r := block.HCLBlock().Body.MissingItemRange()
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required argument",
Detail: `"tags" attribute is required by coder_workspace_tags blocks`,
Subject: &r,
EvalContext: evCtx,
})
continue
} Related, I did the same for // Added code to expose the HCL
func (b *Block) HCLBlock() *hcl.Block {
return b.hclBlock
}
// Usage
func Foo() {
// ...
tagsAttr := block.GetAttribute("tags")
if tagsAttr.IsNil() {
r := block.HCLBlock().Body.MissingItemRange()
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required argument",
Detail: `"tags" attribute is required by coder_workspace_tags blocks`,
Subject: &r,
EvalContext: evCtx,
})
continue
}
// ...
} And lastly to make these added diagnostic errors to work, I had to expose the func (p *Parser) Files() map[string]*hcl.File {
return p.underlying.Files()
} It just makes sense for my use case to use the same hcl diagnostic errors, rather than making some new custom error type. And I can leverage the same error printers already provided in the HCL packages. |
Signed-off-by: nikpivkin <[email protected]>
I see what you mean by using |
@nikpivkin As for naming of a function to return the underlying raw HCL. Just a suggestion based on a recent finding.
So |
Can we do this in a separate PR? It seems unrelated to the current PR. Feel free to open a new PR. |
@simar7 A separate PR for sure. I'll open one 👍 |
@nikpivkin I think this PR has gotten overlooked in the conversations. |
@simar7 Can you take a look? |
fooAttr := foo.GetAttribute("attr") | ||
b, err := root.GetReferencedBlock(fooAttr, foo) | ||
require.NoError(t, err) | ||
require.NotNil(t, b) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can strengthen this assertion?
require.NotNil(t, b) | |
assert.Equal(t, "test", b.Values().AsValueSlice()[0].AsString()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, left one comment. I'd also get @nikpivkin to take a look at this again.
Description
References included in hcl objects should be returned from
'Attribute.AllReferences()'
. Traverse the object fields recursively to locate references.Expressions such as:
Should return
variable.cache
in theAllReferences()
Checklist