Skip to content

Commit 54411be

Browse files
dbanckradeksimko
authored andcommitted
Add support for query subcommand (#525)
1 parent 7ae12dd commit 54411be

File tree

8 files changed

+272
-0
lines changed

8 files changed

+272
-0
lines changed

tfexec/internal/e2etest/query_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package e2etest
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"io"
10+
"regexp"
11+
"strings"
12+
"testing"
13+
14+
"github.com/hashicorp/go-version"
15+
"github.com/hashicorp/terraform-exec/tfexec"
16+
"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
17+
)
18+
19+
func TestQueryJSON_TF112(t *testing.T) {
20+
versions := []string{testutil.Latest_v1_12}
21+
22+
runTestWithVersions(t, versions, "query", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
23+
err := tf.Init(context.Background())
24+
if err != nil {
25+
t.Fatalf("error running Init in test directory: %s", err)
26+
}
27+
28+
re := regexp.MustCompile("terraform query -json was added in 1.14.0")
29+
30+
err = tf.QueryJSON(context.Background(), io.Discard)
31+
if err != nil && !re.MatchString(err.Error()) {
32+
t.Fatalf("error running Query: %s", err)
33+
}
34+
})
35+
}
36+
37+
func TestQueryJSON_TF114(t *testing.T) {
38+
versions := []string{testutil.Latest_Alpha_v1_14}
39+
40+
runTestWithVersions(t, versions, "query", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
41+
err := tf.Init(context.Background())
42+
if err != nil {
43+
t.Fatalf("error running Init in test directory: %s", err)
44+
}
45+
46+
var output bytes.Buffer
47+
err = tf.QueryJSON(context.Background(), &output)
48+
if err != nil {
49+
t.Fatalf("error running Query: %s", err)
50+
}
51+
52+
results := strings.Count(output.String(), "list.concept_pet.pets: Result found")
53+
if results != 5 {
54+
t.Fatalf("expected 5 query results, but got %d", results)
55+
}
56+
})
57+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
terraform {
2+
required_providers {
3+
concept = {
4+
source = "dbanck/concept"
5+
version = "0.1.0"
6+
}
7+
}
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
list "concept_pet" "pets" {
2+
provider = concept
3+
}

tfexec/internal/testutil/tfcache.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
Latest_Alpha_v1_10 = "1.10.0-alpha20240926"
3737
Latest_v1_11 = "1.11.4"
3838
Latest_v1_12 = "1.12.2"
39+
Latest_Alpha_v1_14 = "1.14.0-alpha20250903"
3940
)
4041

4142
const appendUserAgent = "tfexec-testutil"

tfexec/options.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@ func FromModule(source string) *FromModuleOption {
184184
return &FromModuleOption{source}
185185
}
186186

187+
type GenerateConfigOutOption struct {
188+
path string
189+
}
190+
191+
func GenerateConfigOut(path string) *GenerateConfigOutOption {
192+
return &GenerateConfigOutOption{path}
193+
}
194+
187195
type GetOption struct {
188196
get bool
189197
}

tfexec/query.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfexec
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"io"
10+
"os/exec"
11+
)
12+
13+
type queryConfig struct {
14+
dir string
15+
generateConfig string
16+
reattachInfo ReattachInfo
17+
vars []string
18+
varFiles []string
19+
}
20+
21+
var defaultQueryOptions = queryConfig{}
22+
23+
// QueryOption represents options used in the Query method.
24+
type QueryOption interface {
25+
configureQuery(*queryConfig)
26+
}
27+
28+
func (opt *DirOption) configureQuery(conf *queryConfig) {
29+
conf.dir = opt.path
30+
}
31+
32+
func (opt *GenerateConfigOutOption) configureQuery(conf *queryConfig) {
33+
conf.generateConfig = opt.path
34+
}
35+
36+
func (opt *ReattachOption) configureQuery(conf *queryConfig) {
37+
conf.reattachInfo = opt.info
38+
}
39+
40+
func (opt *VarFileOption) configureQuery(conf *queryConfig) {
41+
conf.varFiles = append(conf.varFiles, opt.path)
42+
}
43+
44+
func (opt *VarOption) configureQuery(conf *queryConfig) {
45+
conf.vars = append(conf.vars, opt.assignment)
46+
}
47+
48+
// QueryJSON executes `terraform query` with the specified options as well as the
49+
// `-json` flag and waits for it to complete.
50+
//
51+
// Using the `-json` flag will result in
52+
// [machine-readable](https://developer.hashicorp.com/terraform/internals/machine-readable-ui)
53+
// JSON being written to the supplied `io.Writer`.
54+
//
55+
// The returned error is nil if `terraform query` has been executed and exits
56+
// with 0.
57+
//
58+
// QueryJSON is likely to be removed in a future major version in favour of
59+
// query returning JSON by default.
60+
func (tf *Terraform) QueryJSON(ctx context.Context, w io.Writer, opts ...QueryOption) error {
61+
err := tf.compatible(ctx, tf1_14_0, nil)
62+
if err != nil {
63+
return fmt.Errorf("terraform query -json was added in 1.14.0: %w", err)
64+
}
65+
66+
tf.SetStdout(w)
67+
68+
cmd, err := tf.queryJSONCmd(ctx, opts...)
69+
if err != nil {
70+
return err
71+
}
72+
73+
err = tf.runTerraformCmd(ctx, cmd)
74+
if err != nil {
75+
return err
76+
}
77+
78+
return nil
79+
}
80+
81+
func (tf *Terraform) queryJSONCmd(ctx context.Context, opts ...QueryOption) (*exec.Cmd, error) {
82+
c := defaultQueryOptions
83+
84+
for _, o := range opts {
85+
o.configureQuery(&c)
86+
}
87+
88+
args, err := tf.buildQueryArgs(ctx, c)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
args = append(args, "-json")
94+
95+
return tf.buildQueryCmd(ctx, c, args)
96+
}
97+
98+
func (tf *Terraform) buildQueryArgs(ctx context.Context, c queryConfig) ([]string, error) {
99+
args := []string{"query", "-no-color"}
100+
101+
if c.generateConfig != "" {
102+
args = append(args, "-generate-config-out="+c.generateConfig)
103+
}
104+
105+
for _, vf := range c.varFiles {
106+
args = append(args, "-var-file="+vf)
107+
}
108+
109+
if c.vars != nil {
110+
for _, v := range c.vars {
111+
args = append(args, "-var", v)
112+
}
113+
}
114+
115+
return args, nil
116+
}
117+
118+
func (tf *Terraform) buildQueryCmd(ctx context.Context, c queryConfig, args []string) (*exec.Cmd, error) {
119+
// optional positional argument
120+
if c.dir != "" {
121+
args = append(args, c.dir)
122+
}
123+
124+
mergeEnv := map[string]string{}
125+
if c.reattachInfo != nil {
126+
reattachStr, err := c.reattachInfo.marshalString()
127+
if err != nil {
128+
return nil, err
129+
}
130+
mergeEnv[reattachEnvVar] = reattachStr
131+
}
132+
133+
return tf.buildTerraformCmd(ctx, mergeEnv, args...), nil
134+
}

tfexec/query_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfexec
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
11+
)
12+
13+
func TestQueryJSONCmd(t *testing.T) {
14+
td := t.TempDir()
15+
16+
tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_Alpha_v1_14))
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
21+
// empty env, to avoid environ mismatch in testing
22+
tf.SetEnv(map[string]string{})
23+
24+
t.Run("defaults", func(t *testing.T) {
25+
queryCmd, err := tf.queryJSONCmd(context.Background())
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
30+
assertCmd(t, []string{
31+
"query",
32+
"-no-color",
33+
"-json",
34+
}, nil, queryCmd)
35+
})
36+
37+
t.Run("override all", func(t *testing.T) {
38+
queryCmd, err := tf.queryJSONCmd(context.Background(),
39+
GenerateConfigOut("generated.tf"),
40+
Var("android=paranoid"),
41+
Var("brain_size=planet"),
42+
VarFile("trillian"),
43+
Dir("earth"))
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
48+
assertCmd(t, []string{
49+
"query",
50+
"-no-color",
51+
"-generate-config-out=generated.tf",
52+
"-var-file=trillian",
53+
"-var", "android=paranoid",
54+
"-var", "brain_size=planet",
55+
"-json",
56+
"earth",
57+
}, nil, queryCmd)
58+
})
59+
}

tfexec/version.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ var (
3434
tf1_4_0 = version.Must(version.NewVersion("1.4.0"))
3535
tf1_6_0 = version.Must(version.NewVersion("1.6.0"))
3636
tf1_9_0 = version.Must(version.NewVersion("1.9.0"))
37+
tf1_13_0 = version.Must(version.NewVersion("1.13.0"))
38+
tf1_14_0 = version.Must(version.NewVersion("1.14.0"))
3739
)
3840

3941
// Version returns structured output from the terraform version command including both the Terraform CLI version

0 commit comments

Comments
 (0)