Skip to content

Commit

Permalink
Allow CUE content and paths to be evaluated together
Browse files Browse the repository at this point in the history
* There is a way for CUE to evaluate string expressions and path-based
expressions in a single build, by using the loader with an Overlay option
so that it will load the content as though it were a file
* Drop the restriction that `content` and `paths` be mutually
exclusive, you can set both. Add an example and docs
  • Loading branch information
dghubble committed May 26, 2024
1 parent bc647ff commit 3b92d9e
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 97 deletions.
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ terraform {
required_providers {
ct = {
source = "poseidon/cue"
version = "0.2.0"
version = "0.4.0"
}
}
}
Expand All @@ -40,29 +40,46 @@ Define a `cue_config` data source to validate CUE `content`.
```tf
data "cue_config" "example" {
pretty_print = true
content = <<EOF
a: 1
b: 2
sum: a + b
_hidden: 3
l: [a, b]
map: [string]:int
map: {a: 1 * 5}
map: {"b": b * 5}
EOF
content = <<-EOT
a: 1
b: 2
sum: a + b
_hidden: 3
l: [a, b]
map: [string]:int
map: {a: 1 * 5}
map: {"b": b * 5}
EOT
}
```

Alternately, provide `paths` to CUE files (supports imports).
Optionally provide `paths` to CUE files (supports imports).

```tf
data "cue_config" "example" {
paths = [
"core.cue",
"box.cue",
]
pretty_print = false
}
```

Or unify `content` and `path` based expressions together.

```tf
data "cue_config" "example" {
paths = [
"partial.cue",
]
content = <<-EOT
package example
_config: {
name: "ACME"
amount: "$20.00"
}
EOT
}
```

Expand Down
45 changes: 31 additions & 14 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ terraform {
required_providers {
ct = {
source = "poseidon/cue"
version = "0.2.0"
version = "0.4.0"
}
}
}
Expand All @@ -29,30 +29,47 @@ Define a `cue_config` data source to validate CUE `content`.

```tf
data "cue_config" "example" {
content = <<EOF
a: 1
b: 2
sum: a + b
_hidden: 3
l: [a, b]
map: [string]:int
map: {a: 1 * 5}
map: {"b": b * 5}
EOF
pretty_print = true
content = <<-EOT
a: 1
b: 2
sum: a + b
_hidden: 3
l: [a, b]
map: [string]:int
map: {a: 1 * 5}
map: {"b": b * 5}
EOT
}
```

Alternately, provide `paths` to CUE files (supports imports).
Optionally provide `paths` to CUE files (supports imports).

```tf
data "cue_config" "example" {
paths = [
"core.cue",
"box.cue",
]
pretty_print = false
}
```

Or unify `content` and `path` based expressions together.

```tf
data "cue_config" "example" {
paths = [
"partial.cue",
]
content = <<-EOT
package example
_config: {
name: "ACME"
amount: "$20.00"
}
EOT
}
```

Expand Down
49 changes: 33 additions & 16 deletions examples/example.tf
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
data "cue_config" "example" {
content = <<EOF
a: 1
b: 2
sum: a + b
_hidden: 3
l: [a, b]
content = <<-EOT
a: 1
b: 2
sum: a + b
_hidden: 3
l: [a, b]
map: [string]:int
map: {a: 1 * 5}
map: {"b": b * 5}
EOF
}

output "out" {
description = "Show Cue rendered as JSON"
value = data.cue_config.example.rendered
map: [string]:int
map: {a: 1 * 5}
map: {"b": b * 5}
EOT
}

data "cue_config" "example2" {
Expand All @@ -24,8 +19,30 @@ data "cue_config" "example2" {
]
}

data "cue_config" "example3" {
paths = [
"partial.cue",
]
content = <<-EOT
package example
_config: {
name: "a name"
}
EOT
}

output "out" {
description = "Show Cue content rendered as JSON"
value = data.cue_config.example.rendered
}

output "out2" {
description = "Show Cue rendered as JSON"
description = "Show Cue files rendered as JSON"
value = data.cue_config.example2.rendered
}

output "out3" {
description = "Show Cue content+files rendered as JSON"
value = data.cue_config.example3.rendered
}
Empty file added examples/outputs.tf
Empty file.
7 changes: 7 additions & 0 deletions examples/partial.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package examples

{
title: "Invoice"
customer: _config.name
bill: _config.amount
}
21 changes: 10 additions & 11 deletions internal/data_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,16 @@ func dataConfigRead(ctx context.Context, d *schema.ResourceData, meta any) diag.
}
}

// only allow content xor paths (not both)
_, hasContent := d.GetOk("content")
_, hasPaths := d.GetOk("paths")
if hasContent && hasPaths {
return diag.FromErr(fmt.Errorf("content and paths are mutually exclusive"))
}

// create a Cue context
cuectx := cuecontext.New()

var value cue.Value
var err error
if content != "" {
if len(paths) < 1 {
value = cuectx.CompileString(content)
} else {
// load cue "instances" from fs
value, err = loadPaths(cuectx, d, paths)
// load cue "instances" from filesystem
value, err = loadPaths(cuectx, d, content, paths)
if err != nil {
return diag.FromErr(err)
}
Expand Down Expand Up @@ -116,12 +109,18 @@ func dataConfigRead(ctx context.Context, d *schema.ResourceData, meta any) diag.
}

// load Paths parses Cue files and merges them.
func loadPaths(cuectx *cue.Context, data *schema.ResourceData, paths []string) (cue.Value, error) {
func loadPaths(cuectx *cue.Context, data *schema.ResourceData, content string, paths []string) (cue.Value, error) {
dir := data.Get("dir").(string)

config := &load.Config{
Dir: dir,
// Trick CUE into "loading" the content expression as though it was a file
Overlay: map[string]load.Source{
"/content.cue": load.FromString(content),
},
}
// content.cue is a fake path to convince load to read string contents
paths = append(paths, "/content.cue")

// load cue "instances" from the given paths
instances := load.Instances(paths, config)
Expand Down
86 changes: 44 additions & 42 deletions internal/data_config_test.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
package internal

import (
"regexp"
"testing"

r "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

const cueWithContent = `
data "cue_config" "example" {
content = <<EOT
a: 1
b: 2
_hidden: 3
sum: a + b
l: [a, b]
content = <<-EOT
a: 1
b: 2
_hidden: 3
sum: a + b
l: [a, b]
map: [string]:int
map: {a: 1*5}
map: {"b": b*5}
EOT
map: [string]:int
map: {a: 1*5}
map: {"b": b*5}
EOT
}
`

const cueWithContentPretty = `
data "cue_config" "example" {
content = <<EOT
a: 1
b: 2
_hidden: 3
sum: a + b
l: [a, b]
map: [string]:int
map: {a: 1*5}
map: {"b": b*5}
EOT
pretty_print = true
content = <<-EOT
a: 1
b: 2
_hidden: 3
sum: a + b
l: [a, b]
map: [string]:int
map: {a: 1*5}
map: {"b": b*5}
EOT
}
`

Expand Down Expand Up @@ -67,6 +66,23 @@ data "cue_config" "example" {

const outputWithPaths = `{"layout":{"boxes":[{"color":"red","row":0,"column":0},{"color":"blue","row":0,"column":1},{"color":"green","row":1,"column":0},{"color":"yellow","row":1,"column":1}]},"a":1,"b":2,"sum":3,"l":[1,2],"map":{"a":5,"b":10},"ben":{"name":"Ben","age":31,"human":true}}`

const cueWithContentAndPaths = `
data "cue_config" "example" {
paths = [
"../examples/partial.cue",
]
content = <<-EOT
package examples
_config: {
name: "ACME"
amount: "$20.00"
}
EOT
}
`
const outputWithContentAndPaths = `{"title":"Invoice","customer":"ACME","bill":"$20.00"}`

const cueWithDir = `
data "cue_config" "example" {
paths = [
Expand Down Expand Up @@ -120,6 +136,12 @@ func TestConfigRender(t *testing.T) {
r.TestCheckResourceAttr("data.cue_config.example", "rendered", outputWithPaths),
),
},
{
Config: cueWithContentAndPaths,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("data.cue_config.example", "rendered", outputWithContentAndPaths),
),
},
{
Config: cueWithDir,
Check: r.ComposeTestCheckFunc(
Expand All @@ -141,23 +163,3 @@ func TestConfigRender(t *testing.T) {
},
})
}

func TestConfigContentOrPaths(t *testing.T) {
hcl := `
data "cue_config" "invalid" {
content = "a: 1"
paths = [
"../examples/core.cue",
]
}
`
r.UnitTest(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
{
Config: hcl,
ExpectError: regexp.MustCompile("are mutually exclusive"),
},
},
})
}

0 comments on commit 3b92d9e

Please sign in to comment.