Skip to content

Commit

Permalink
lang/funcs: add "all" function
Browse files Browse the repository at this point in the history
# What

This commit adds an `all` function to Terraform configuration. A
reason we might want this function is because it will enable more
powerful custom variable validations. For example:

```hcl
variable "amis" {
  type = list(object({
    id = string
  }))

  validation {
    condition = (all([
      for a in var.amis : length(a.id) > 4 && substr(a.id, 0, 4) == "ami-"
    ]))
    error_message = "The ID of at least one AMI was invalid."
  }
}
```

If this is not the intended use of custom variable validation, the `all`
function may still prove useful in other contexts where we, for example,
create a resource conditionally.
  • Loading branch information
artburkart committed Jul 29, 2020
1 parent 5734a0c commit 8b218ee
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 0 deletions.
41 changes: 41 additions & 0 deletions lang/funcs/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,41 @@ var LengthFunc = function.New(&function.Spec{
},
})

// AllFunc constructs a function that returns true if all elements of the
// collection are true or "true". If the collection is empty, return true.
var AllFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "collection",
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
ty := args[0].Type()
if !ty.IsListType() && !ty.IsTupleType() && !ty.IsSetType() {
return cty.NilVal, errors.New("argument must be list, tuple, or set")
}

tobool := MakeToFunc(cty.Bool)
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
got, err := tobool.Call([]cty.Value{v})
if err != nil {
return cty.False, nil
}
eq, err := stdlib.Equal(got, cty.True)
if err != nil {
return cty.NilVal, err
}
if eq.False() {
return cty.False, nil
}
}
return cty.True, nil
},
})

// CoalesceFunc constructs a function that takes any number of arguments and
// returns the first one that isn't empty. This function was copied from go-cty
// stdlib and modified so that it returns the first *non-empty* non-null element
Expand Down Expand Up @@ -582,6 +617,12 @@ func Length(collection cty.Value) (cty.Value, error) {
return LengthFunc.Call([]cty.Value{collection})
}

// All returns true if all elements of the collection are true or "true".
// If the collection is empty, return true.
func All(collection cty.Value) (cty.Value, error) {
return AllFunc.Call([]cty.Value{collection})
}

// Coalesce takes any number of arguments and returns the first one that isn't empty.
func Coalesce(args ...cty.Value) (cty.Value, error) {
return CoalesceFunc.Call(args)
Expand Down
93 changes: 93 additions & 0 deletions lang/funcs/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,99 @@ func TestLength(t *testing.T) {
}
}

func TestAll(t *testing.T) {
tests := []struct {
Collection cty.Value
Want cty.Value
Err bool
}{
{
cty.ListValEmpty(cty.String),
cty.True,
false,
},
{
cty.TupleVal([]cty.Value{}),
cty.True,
false,
},
{
cty.SetValEmpty(cty.Bool),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.True}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.StringVal("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.NumberIntVal(1)}),
cty.False,
false,
},
{
cty.StringVal("true"),
cty.False,
true,
},
// {
// cty.ListVal([]cty.Value{cty.ListValEmpty(cty.String)}),
// cty.False,
// false,
// },
// {
// cty.ListVal([]cty.Value{cty.True, cty.StringVal("true")}),
// cty.True,
// false,
// },
// {
// cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
// cty.False,
// false,
// },
}

for _, test := range tests {
t.Run(fmt.Sprintf("all(%#v)", test.Collection), func(t *testing.T) {
got, err := All(test.Collection)

if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

func TestCoalesce(t *testing.T) {
tests := []struct {
Values []cty.Value
Expand Down
1 change: 1 addition & 0 deletions lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (s *Scope) Functions() map[string]function.Function {
s.funcs = map[string]function.Function{
"abs": stdlib.AbsoluteFunc,
"abspath": funcs.AbsPathFunc,
"all": funcs.AllFunc,
"basename": funcs.BasenameFunc,
"base64decode": funcs.Base64DecodeFunc,
"base64encode": funcs.Base64EncodeFunc,
Expand Down
7 changes: 7 additions & 0 deletions lang/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ func TestFunctions(t *testing.T) {
},
},

"all": {
{
`all([true])`,
cty.True,
},
},

"base64decode": {
{
`base64decode("YWJjMTIzIT8kKiYoKSctPUB+")`,
Expand Down
30 changes: 30 additions & 0 deletions website/docs/configuration/functions/all.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
layout: functions
page_title: all - Functions - Configuration Language
sidebar_current: docs-funcs-collection-all
description: |-
The all function determines whether all elements of a collection are true or
"true". If the collection is empty, it returns true.
---

# `all` Function

-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and
earlier, see
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).

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

```hcl
all(list)
```

## Examples

```command
> all(["true", true])
true
> all([true, false])
false
```
4 changes: 4 additions & 0 deletions website/layouts/functions.erb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@
<a href="#docs-funcs-collection">Collection Functions</a>
<ul class="nav">

<li>
<a href="/docs/configuration/functions/all.html">all</a>
</li>

<li>
<a href="/docs/configuration/functions/chunklist.html">chunklist</a>
</li>
Expand Down

0 comments on commit 8b218ee

Please sign in to comment.