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

hcl2template: add alltrue & anytrue function #13237

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions hcl2template/function/alltrue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package function
mogrogan marked this conversation as resolved.
Show resolved Hide resolved

import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

// AllTrue constructs a function that returns true if all elements of the
// list are true. If the list is empty, return true.
var AllTrue = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result := cty.True
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if !v.IsKnown() {
return cty.UnknownVal(cty.Bool), nil
}
if v.IsNull() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we cannot declare the function as not accepting null/unknown? Though this might make sense to support them and return false immediately in that case so please feel free to push back on this comment.

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 think that user of this function have good chance of being using terraform and I think we should keep consistency with the terraform project to reduce the risk of having confusing error.

return cty.False, nil
}
result = result.And(v)
if result.False() {
return cty.False, nil
}
}
return result, nil
},
})
89 changes: 89 additions & 0 deletions hcl2template/function/alltrue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package function

import (
"fmt"
"testing"

"github.com/zclconf/go-cty/cty"
)

func TestAllTrue(t *testing.T) {
tests := []struct {
Collection cty.Value
Want cty.Value
Err bool
}{
{
cty.ListValEmpty(cty.Bool),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.True}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.False}),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.True, cty.False}),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.False, cty.True}),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.True, cty.NullVal(cty.Bool)}),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
cty.UnknownVal(cty.Bool).RefineNotNull(),
false,
},
{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.Bool),
cty.UnknownVal(cty.Bool),
}),
cty.UnknownVal(cty.Bool).RefineNotNull(),
false,
},
{
cty.UnknownVal(cty.List(cty.Bool)),
cty.UnknownVal(cty.Bool).RefineNotNull(),
false,
},
{
cty.NullVal(cty.List(cty.Bool)),
cty.NilVal,
true,
},
}

for _, tc := range tests {
t.Run(fmt.Sprintf("alltrue(%#v)", tc.Collection), func(t *testing.T) {
got, err := AllTrue.Call([]cty.Value{tc.Collection})

if tc.Err && err == nil {
t.Fatal("succeeded; want error")
}
if !tc.Err && err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(tc.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, tc.Want)
}
})
}
}
44 changes: 44 additions & 0 deletions hcl2template/function/anytrue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package function

import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

// AnyTrue constructs a function that returns true if a single element of
// the list is true. If the list is empty, return false.
var AnyTrue = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result := cty.False
var hasUnknown bool
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if !v.IsKnown() {
hasUnknown = true
continue
}
if v.IsNull() {
continue
}
result = result.Or(v)
if result.True() {
return cty.True, nil
}
}
if hasUnknown {
return cty.UnknownVal(cty.Bool), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume this behaviour was lifted from Terraform, but I find this somewhat debatable? Since the function is called AnyTrue, if one value is effectively true, I don't think we should care about one value being unknown in there

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 agree with you, when I took that from terraform I found it a bit weird but I think the consistency between packer and terraform is more important unfortunately.

}
return result, nil
},
})
89 changes: 89 additions & 0 deletions hcl2template/function/anytrue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package function

import (
"fmt"
"testing"

"github.com/zclconf/go-cty/cty"
)

func TestAnyTrue(t *testing.T) {
tests := []struct {
Collection cty.Value
Want cty.Value
Err bool
}{
{
cty.ListValEmpty(cty.Bool),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.True}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.False}),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.True, cty.False}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.False, cty.True}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.True, cty.NullVal(cty.Bool)}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
cty.UnknownVal(cty.Bool).RefineNotNull(),
false,
},
{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.Bool),
cty.UnknownVal(cty.Bool),
}),
cty.UnknownVal(cty.Bool).RefineNotNull(),
false,
},
{
cty.UnknownVal(cty.List(cty.Bool)),
cty.UnknownVal(cty.Bool).RefineNotNull(),
false,
},
{
cty.NullVal(cty.List(cty.Bool)),
cty.NilVal,
true,
},
}

for _, tc := range tests {
t.Run(fmt.Sprintf("anytrue(%#v)", tc.Collection), func(t *testing.T) {
got, err := AnyTrue.Call([]cty.Value{tc.Collection})

if tc.Err && err == nil {
t.Fatal("succeeded; want error")
}
if !tc.Err && err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(tc.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, tc.Want)
}
})
}
}
2 changes: 2 additions & 0 deletions hcl2template/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func Functions(basedir string) map[string]function.Function {
funcs := map[string]function.Function{
"abs": stdlib.AbsoluteFunc,
"abspath": filesystem.AbsPathFunc,
"alltrue": pkrfunction.AllTrue,
"anytrue": pkrfunction.AnyTrue,
"aws_secretsmanager": pkrfunction.AWSSecret,
"basename": filesystem.BasenameFunc,
"base64decode": encoding.Base64DecodeFunc,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
page_title: alltrue - Functions - Configuration Language
description: |-
The alltrue function determines whether all elements of a collection
are true or "true". If the collection is empty, it returns true.
---

# `alltrue` Function

`alltrue` returns `true` if all elements in a given collection are `true`
or `"true"`. It also returns `true` if the collection is empty.

```hcl
alltrue(list)
```

## Examples

```command
> alltrue(["true", true])
true
> alltrue([true, false])
false
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
page_title: anytrue - Functions - Configuration Language
description: |-
The anytrue function determines whether any element of a collection
is true or "true". If the collection is empty, it returns false.
---

# `anytrue` Function

`anytrue` returns `true` if any element in a given collection is `true`
or `"true"`. It also returns `false` if the collection is empty.

```hcl
anytrue(list)
```

## Examples

```command
> anytrue(["true"])
true
> anytrue([true])
true
> anytrue([true, false])
true
> anytrue([])
false
```
8 changes: 8 additions & 0 deletions website/data/docs-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@
{
"title": "Collection Functions",
"routes": [
{
"title": "alltrue",
"path": "templates/hcl_templates/functions/collection/alltrue"
},
{
"title": "anytrue",
"path": "templates/hcl_templates/functions/collection/anytrue"
},
{
"title": "chunklist",
"path": "templates/hcl_templates/functions/collection/chunklist"
Expand Down
Loading