Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 3 additions & 1 deletion internal/backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *confi
// If validation is enabled, validate
if b.OpValidation {
log.Printf("[TRACE] backend/local: running validation operation")
validateDiags := ret.Core.Validate(ret.Config, nil)
// TODO: Implement query validate command. op.Query is false when running the command "terraform validate"
opts := &terraform.ValidateOpts{Query: op.Query}
validateDiags := ret.Core.Validate(ret.Config, opts)
diags = diags.Append(validateDiags)
}
}
Expand Down
75 changes: 75 additions & 0 deletions internal/terraform/context_apply2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4271,3 +4271,78 @@ resource "test_resource" "foo" {
t.Fatalf("missing identity in state, got %q", fooState.Current.IdentityJSON)
}
}

func TestContext2Apply_noListValidated(t *testing.T) {
tests := map[string]struct {
name string
mainConfig string
queryConfig string
query bool
}{
"query files not validated in default validate mode": {
mainConfig: `
terraform {
required_providers {
test = {
source = "hashicorp/test"
version = "1.0.0"
}
}
}
`,
queryConfig: `
// This config is invalid, but should not be validated in default validate mode
list "test_resource" "test" {
provider = test
config {
filter = {
attr = list.non_existent.attr
}
}
}
locals {
test = list.non_existent.attr
}
`,
query: false,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
configFiles := map[string]string{"main.tf": tc.mainConfig}
if tc.queryConfig != "" {
configFiles["main.tfquery.hcl"] = tc.queryConfig
}

opts := []configs.Option{}
if tc.query {
opts = append(opts, configs.MatchQueryFiles())
}

m := testModuleInline(t, configFiles, opts...)

p := testProvider("test")
p.GetProviderSchemaResponse = getListProviderSchemaResp()

ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})

diags := ctx.Validate(m, &ValidateOpts{
Query: tc.query,
})
tfdiags.AssertNoErrors(t, diags)

plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
tfdiags.AssertNoErrors(t, diags)

_, diags = ctx.Apply(plan, m, nil)
tfdiags.AssertNoErrors(t, diags)
})
}
}
24 changes: 15 additions & 9 deletions internal/terraform/context_plan_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,11 @@ list "test_resource" "test1" {
},
},
"query run, action references resource": {
toBeImplemented: true, // TODO: Fix the graph built by query operations.
module: map[string]string{
"main.tf": `
action "test_action" "hello" {
config {
attr = resource.test_object.a
attr = resource.test_object.a.name
}
}
resource "test_object" "a" {
Expand Down Expand Up @@ -3313,7 +3312,17 @@ resource "test_object" "a" {
t.Skip("Test not implemented yet")
}

m := testModuleInline(t, tc.module)
opts := SimplePlanOpts(plans.NormalMode, InputValues{})
if tc.planOpts != nil {
opts = tc.planOpts
}

configOpts := []configs.Option{}
if opts.Query {
configOpts = append(configOpts, configs.MatchQueryFiles())
}

m := testModuleInline(t, tc.module, configOpts...)

p := &testing_provider.MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Expand Down Expand Up @@ -3444,7 +3453,9 @@ resource "test_object" "a" {
},
})

diags := ctx.Validate(m, &ValidateOpts{})
diags := ctx.Validate(m, &ValidateOpts{
Query: opts.Query,
})
if tc.expectValidateDiagnostics != nil {
tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectValidateDiagnostics(m))
} else if tc.assertValidateDiagnostics != nil {
Expand All @@ -3462,11 +3473,6 @@ resource "test_object" "a" {
prevRunState = states.BuildState(tc.buildState)
}

opts := SimplePlanOpts(plans.NormalMode, InputValues{})
if tc.planOpts != nil {
opts = tc.planOpts
}

plan, diags := ctx.Plan(m, prevRunState, opts)

if tc.expectPlanDiagnostics != nil {
Expand Down
101 changes: 94 additions & 7 deletions internal/terraform/context_plan_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
Expand Down Expand Up @@ -885,16 +886,100 @@ func TestContext2Plan_queryList(t *testing.T) {
}
},
},
{
name: ".tf file blocks should not be evaluated in query mode",
mainConfig: `
terraform {
required_providers {
test = {
source = "hashicorp/test"
version = "1.0.0"
}
}
}

locals {
foo = "bar"
}

// This would produce a plan error if triggered, but we expect it to be ignored in query mode
resource "test_resource" "example" {
instance_type = "ami-123456"

lifecycle {
precondition {
condition = local.foo != "bar"
error_message = "This should not be executed"
}
}
}

`,
queryConfig: `
list "test_resource" "test" {
provider = test
include_resource = true

config {
filter = {
attr = "foo"
}
}
}
`,
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
madeUp := []cty.Value{
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-654321")}),
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-789012")}),
}
ids := []cty.Value{}
for i := range madeUp {
ids = append(ids, cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal(fmt.Sprintf("i-v%d", i+1)),
}))
}

resp := []cty.Value{}
for i, v := range madeUp {
mp := map[string]cty.Value{
"identity": ids[i],
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
}
if request.IncludeResourceObject {
mp["state"] = v
}
resp = append(resp, cty.ObjectVal(mp))
}

ret := request.Config.AsValueMap()
maps.Copy(ret, map[string]cty.Value{
"data": cty.TupleVal(resp),
})

return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
},
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
expectedResources := []string{"list.test_resource.test"}
actualResources := make([]string, 0)
for _, change := range changes.Queries {
actualResources = append(actualResources, change.Addr.String())
}
if diff := cmp.Diff(expectedResources, actualResources); diff != "" {
t.Fatalf("Expected resources to match, but they differ: %s", diff)
}
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
configs := map[string]string{"main.tf": tc.mainConfig}
configFiles := map[string]string{"main.tf": tc.mainConfig}
if tc.queryConfig != "" {
configs["main.tfquery.hcl"] = tc.queryConfig
configFiles["main.tfquery.hcl"] = tc.queryConfig
}

mod := testModuleInline(t, configs)
mod := testModuleInline(t, configFiles, configs.MatchQueryFiles())
providerAddr := addrs.NewDefaultProvider("test")
provider := testProvider("test")
provider.ConfigureProvider(providers.ConfigureProviderRequest{})
Expand Down Expand Up @@ -922,7 +1007,9 @@ func TestContext2Plan_queryList(t *testing.T) {
})
tfdiags.AssertNoDiagnostics(t, diags)

diags = ctx.Validate(mod, &ValidateOpts{})
diags = ctx.Validate(mod, &ValidateOpts{
Query: true,
})
if tc.assertValidateDiags != nil {
tc.assertValidateDiags(t, diags)
return
Expand Down Expand Up @@ -1040,10 +1127,10 @@ func TestContext2Plan_queryListArgs(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
configs := map[string]string{"main.tf": mainConfig}
configs["main.tfquery.hcl"] = tc.queryConfig
configFiles := map[string]string{"main.tf": mainConfig}
configFiles["main.tfquery.hcl"] = tc.queryConfig

mod := testModuleInline(t, configs)
mod := testModuleInline(t, configFiles, configs.MatchQueryFiles())

providerAddr := addrs.NewDefaultProvider("test")
provider := testProvider("test")
Expand Down
4 changes: 4 additions & 0 deletions internal/terraform/context_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type ValidateOpts struct {
// not available to this function. Therefore, it is the responsibility of
// the caller to ensure that the provider configurations are valid.
ExternalProviders map[addrs.RootProviderConfig]providers.Interface

// When true, query files will also be validated.
Query bool
}

// Validate performs semantic validation of a configuration, and returns
Expand Down Expand Up @@ -105,6 +108,7 @@ func (c *Context) Validate(config *configs.Config, opts *ValidateOpts) tfdiags.D
Operation: walkValidate,
ExternalProviderConfigs: opts.ExternalProviders,
ImportTargets: c.findImportTargets(config),
queryPlan: opts.Query,
}).Build(addrs.RootModuleInstance)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
Expand Down
Loading
Loading