mirror of
https://github.com/hashicorp/terraform.git
synced 2026-05-28 04:03:27 -04:00
Add terraform query subcommand (TF-25494) (#37174)
* WIP * Reuse plan command for query CLI * Basic CLI output * Only fail a list request on error * poc: store query results in separate field * WIP: odd mixture between JSONs * Fix list references * Separate JSON rendering The structured JSON now only logs a status on which list query is currently running. The new jsonlist package can marshal the query fields of a plan. * Remove matcher * Store results in an extra struct * Structured list result logging * Move list result output into hooks * Add help text and additional flag * Disable query runs with the cloud backend for now * Review feedback
This commit is contained in:
parent
577f8f02ae
commit
2b9d25c7fd
26 changed files with 885 additions and 59 deletions
|
|
@ -445,6 +445,12 @@ func initCommands(
|
|||
}, nil
|
||||
}
|
||||
|
||||
Commands["query"] = func() (cli.Command, error) {
|
||||
return &command.QueryCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Commands["stacks"] = func() (cli.Command, error) {
|
||||
return &command.StacksCommand{
|
||||
Meta: meta,
|
||||
|
|
|
|||
|
|
@ -154,6 +154,9 @@ type Operation struct {
|
|||
// for unmatched import targets and where any generated config should be
|
||||
// written to.
|
||||
GenerateConfigOut string
|
||||
|
||||
// Query is true if the operation should be a query operation
|
||||
Query bool
|
||||
}
|
||||
|
||||
// HasConfig returns true if and only if the operation has a ConfigDir value
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
|
|||
SkipRefresh: op.Type != backendrun.OperationTypeRefresh && !op.PlanRefresh,
|
||||
GenerateConfigPath: op.GenerateConfigOut,
|
||||
DeferralAllowed: op.DeferralAllowed,
|
||||
Query: op.Query,
|
||||
}
|
||||
run.PlanOpts = planOpts
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,15 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backendrun.Operat
|
|||
diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut))
|
||||
}
|
||||
|
||||
if op.Query {
|
||||
// We don't support running a query operation with the cloud backend for now
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Query operations are not supported",
|
||||
fmt.Sprintf("%s does not support query operations at this time.", b.appName),
|
||||
))
|
||||
}
|
||||
|
||||
// Return if there are any errors.
|
||||
if diags.HasErrors() {
|
||||
return nil, diags.Err()
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ package arguments
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
)
|
||||
|
||||
// defaultFlagSet creates a FlagSet with the common settings to override
|
||||
// the flag package's noisy defaults.
|
||||
func defaultFlagSet(name string) *flag.FlagSet {
|
||||
f := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||
f.SetOutput(ioutil.Discard)
|
||||
f.SetOutput(io.Discard)
|
||||
f.Usage = func() {}
|
||||
|
||||
return f
|
||||
|
|
|
|||
75
internal/command/arguments/query.go
Normal file
75
internal/command/arguments/query.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Query represents the command-line arguments for the query command.
|
||||
type Query struct {
|
||||
// State, Operation, and Vars are the common extended flags
|
||||
State *State
|
||||
Operation *Operation
|
||||
Vars *Vars
|
||||
|
||||
// ViewType specifies which output format to use: human or JSON.
|
||||
ViewType ViewType
|
||||
|
||||
// GenerateConfigPath tells Terraform that config should be generated for
|
||||
// the found resources in the query and which path the generated file should
|
||||
// be written to.
|
||||
GenerateConfigPath string
|
||||
}
|
||||
|
||||
func ParseQuery(args []string) (*Query, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
query := &Query{
|
||||
State: &State{},
|
||||
Operation: &Operation{},
|
||||
Vars: &Vars{},
|
||||
}
|
||||
|
||||
cmdFlags := defaultFlagSet("query")
|
||||
cmdFlags.StringVar(&query.GenerateConfigPath, "generate-config-out", "", "generate-config-out")
|
||||
|
||||
varsFlags := NewFlagNameValueSlice("-var")
|
||||
varFilesFlags := varsFlags.Alias("-var-file")
|
||||
query.Vars.vars = &varsFlags
|
||||
query.Vars.varFiles = &varFilesFlags
|
||||
cmdFlags.Var(query.Vars.vars, "var", "var")
|
||||
cmdFlags.Var(query.Vars.varFiles, "var-file", "var-file")
|
||||
|
||||
var json bool
|
||||
cmdFlags.BoolVar(&json, "json", false, "json")
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
args = cmdFlags.Args()
|
||||
|
||||
if len(args) > 0 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Too many command line arguments",
|
||||
"To specify a working directory for the query, use the global -chdir flag.",
|
||||
))
|
||||
}
|
||||
|
||||
diags = diags.Append(query.Operation.Parse())
|
||||
|
||||
switch {
|
||||
case json:
|
||||
query.ViewType = ViewJSON
|
||||
default:
|
||||
query.ViewType = ViewHuman
|
||||
}
|
||||
|
||||
return query, diags
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
|
||||
|
|
@ -49,12 +51,7 @@ func (plan Plan) getSchema(change jsonplan.ResourceChange) *jsonprovider.Schema
|
|||
|
||||
func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Quality) {
|
||||
checkOpts := func(target plans.Quality) bool {
|
||||
for _, opt := range opts {
|
||||
if opt == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(opts, target)
|
||||
}
|
||||
|
||||
diffs := precomputeDiffs(plan, mode)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ type JSONLog struct {
|
|||
TestFatalInterrupt *viewsjson.TestFatalInterrupt `json:"test_interrupt,omitempty"`
|
||||
TestState *State `json:"test_state,omitempty"`
|
||||
TestPlan *Plan `json:"test_plan,omitempty"`
|
||||
|
||||
ListQueryResult *viewsjson.QueryResult `json:"list_resource_found,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -78,6 +80,10 @@ const (
|
|||
LogTestInterrupt JSONLogType = "test_interrupt"
|
||||
LogTestStatus JSONLogType = "test_status"
|
||||
LogTestRetry JSONLogType = "test_retry"
|
||||
|
||||
// Query Messages
|
||||
LogListStart JSONLogType = "list_start"
|
||||
LogListResourceFound JSONLogType = "list_resource_found"
|
||||
)
|
||||
|
||||
func incompatibleVersions(localVersion, remoteVersion string) bool {
|
||||
|
|
@ -156,7 +162,8 @@ func (renderer Renderer) RenderLog(log *JSONLog) error {
|
|||
LogTestRetry,
|
||||
LogTestPlan,
|
||||
LogTestState,
|
||||
LogTestInterrupt:
|
||||
LogTestInterrupt,
|
||||
LogListStart:
|
||||
// We won't display these types of logs
|
||||
return nil
|
||||
|
||||
|
|
@ -290,6 +297,15 @@ func (renderer Renderer) RenderLog(log *JSONLog) error {
|
|||
}
|
||||
}
|
||||
|
||||
case LogListResourceFound:
|
||||
// TODO: revisit once the cloud backend support list runs
|
||||
// We will need to transform the identity to a more human-readable form
|
||||
result := log.ListQueryResult
|
||||
renderer.Streams.Printf("%s\t%s\t%s\n",
|
||||
result.Address,
|
||||
result.Identity,
|
||||
result.DisplayName)
|
||||
|
||||
default:
|
||||
// If the log type is not a known log type, we will just print the log message
|
||||
renderer.Streams.Println(log.Message)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func (m *Meta) normalizePath(path string) string {
|
|||
// loadConfig reads a configuration from the given directory, which should
|
||||
// contain a root module and have already have any required descendant modules
|
||||
// installed.
|
||||
func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) {
|
||||
func (m *Meta) loadConfig(rootDir string, parserOpts ...configs.Option) (*configs.Config, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
rootDir = m.normalizePath(rootDir)
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics)
|
|||
return nil, diags
|
||||
}
|
||||
|
||||
config, hclDiags := loader.LoadConfig(rootDir)
|
||||
config, hclDiags := loader.LoadConfig(rootDir, parserOpts...)
|
||||
diags = diags.Append(hclDiags)
|
||||
return config, diags
|
||||
}
|
||||
|
|
|
|||
221
internal/command/query.go
Normal file
221
internal/command/query.go
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/backend/backendrun"
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/views"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
type QueryCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *QueryCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform [global options] query [options]
|
||||
|
||||
Queries the remote infrastructure for resources.
|
||||
|
||||
Terraform will search for .tfquery.hcl files within the current configuration.
|
||||
Terraform will then use the configured providers to query the remote
|
||||
infrastructure for resources that match the defined list blocks. The results
|
||||
will be printed to the terminal and optionally can be used to generate
|
||||
configuration.
|
||||
|
||||
Query Customization Options:
|
||||
|
||||
The following options customize how Terraform will run the query.
|
||||
|
||||
-var 'foo=bar' Set a value for one of the input variables in the query
|
||||
file of the configuration. Use this option more than
|
||||
once to set more than one variable.
|
||||
|
||||
-var-file=filename Load variable values from the given file, in addition
|
||||
to the default files terraform.tfvars and *.auto.tfvars.
|
||||
Use this option more than once to include more than one
|
||||
variables file.
|
||||
|
||||
Other Options:
|
||||
|
||||
-generate-config-out=path Instructs Terraform to generate import and resource
|
||||
blocks for any found results. The configuration is
|
||||
written to a new file at PATH, which must not
|
||||
already exist. When this option is used with the
|
||||
json option, the generated configuration will be
|
||||
part of the JSON output instead of written to a
|
||||
file.
|
||||
|
||||
-json If specified, machine readable output will be
|
||||
printed in JSON format
|
||||
|
||||
-no-color If specified, output won't contain any color.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *QueryCommand) Synopsis() string {
|
||||
return "Search and list remote infrastructure with Terraform"
|
||||
}
|
||||
|
||||
func (c *QueryCommand) Run(rawArgs []string) int {
|
||||
// Parse and apply global view arguments
|
||||
common, rawArgs := arguments.ParseView(rawArgs)
|
||||
c.View.Configure(common)
|
||||
|
||||
// Propagate -no-color for legacy use of Ui. The remote backend and
|
||||
// cloud package use this; it should be removed when/if they are
|
||||
// migrated to views.
|
||||
c.Meta.color = !common.NoColor
|
||||
c.Meta.Color = c.Meta.color
|
||||
|
||||
// Parse and validate flags
|
||||
args, diags := arguments.ParseQuery(rawArgs)
|
||||
|
||||
// Instantiate the view, even if there are flag errors, so that we render
|
||||
// diagnostics according to the desired view
|
||||
view := views.NewQuery(args.ViewType, c.View)
|
||||
|
||||
if diags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
view.HelpPrompt()
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check for user-supplied plugin path
|
||||
var err error
|
||||
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
||||
diags = diags.Append(err)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// We currently don't support the paralleism flag in the query command,
|
||||
// so we set it to the default value here. This avoids backend errors
|
||||
// that check for deviant values.
|
||||
c.Meta.parallelism = DefaultParallelism
|
||||
|
||||
// Prepare the backend with the backend-specific arguments
|
||||
be, beDiags := c.PrepareBackend(args.State, args.ViewType)
|
||||
b, isRemoteBackend := be.(BackendWithRemoteTerraformVersion)
|
||||
if isRemoteBackend && !b.IsLocalOperations() {
|
||||
diags = diags.Append(c.providerDevOverrideRuntimeWarningsRemoteExecution())
|
||||
} else {
|
||||
diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
|
||||
}
|
||||
diags = diags.Append(beDiags)
|
||||
if diags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Build the operation request
|
||||
opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.GenerateConfigPath)
|
||||
diags = diags.Append(opDiags)
|
||||
if diags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Collect variable value and add them to the operation request
|
||||
diags = diags.Append(c.GatherVariables(opReq, args.Vars))
|
||||
if diags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Before we delegate to the backend, we'll print any warning diagnostics
|
||||
// we've accumulated here, since the backend will start fresh with its own
|
||||
// diagnostics.
|
||||
view.Diagnostics(diags)
|
||||
diags = nil
|
||||
|
||||
// Perform the operation
|
||||
op, err := c.RunOperation(be, opReq)
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
if op.Result != backendrun.OperationSuccess {
|
||||
return op.Result.ExitStatus()
|
||||
}
|
||||
|
||||
return op.Result.ExitStatus()
|
||||
}
|
||||
|
||||
func (c *QueryCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
|
||||
backendConfig, diags := c.loadBackendConfig(".")
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// Load the backend
|
||||
be, beDiags := c.Backend(&BackendOpts{
|
||||
Config: backendConfig,
|
||||
ViewType: viewType,
|
||||
})
|
||||
diags = diags.Append(beDiags)
|
||||
if beDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return be, diags
|
||||
}
|
||||
|
||||
func (c *QueryCommand) OperationRequest(
|
||||
be backendrun.OperationsBackend,
|
||||
view views.Query,
|
||||
viewType arguments.ViewType,
|
||||
generateConfigOut string,
|
||||
) (*backendrun.Operation, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// Build the operation
|
||||
opReq := c.Operation(be, viewType)
|
||||
opReq.Hooks = view.Hooks()
|
||||
opReq.ConfigDir = "."
|
||||
opReq.Type = backendrun.OperationTypePlan
|
||||
opReq.GenerateConfigOut = generateConfigOut
|
||||
opReq.View = view.Operation()
|
||||
opReq.Query = true
|
||||
|
||||
var err error
|
||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %s", err))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return opReq, diags
|
||||
}
|
||||
|
||||
func (c *QueryCommand) GatherVariables(opReq *backendrun.Operation, args *arguments.Vars) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// FIXME the arguments package currently trivially gathers variable related
|
||||
// arguments in a heterogenous slice, in order to minimize the number of
|
||||
// code paths gathering variables during the transition to this structure.
|
||||
// Once all commands that gather variables have been converted to this
|
||||
// structure, we could move the variable gathering code to the arguments
|
||||
// package directly, removing this shim layer.
|
||||
|
||||
varArgs := args.All()
|
||||
items := make([]arguments.FlagNameValue, len(varArgs))
|
||||
for i := range varArgs {
|
||||
items[i].Name = varArgs[i].Name
|
||||
items[i].Value = varArgs[i].Value
|
||||
}
|
||||
c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items}
|
||||
opReq.Variables, diags = c.collectVariableValues()
|
||||
|
||||
return diags
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ package views
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -231,3 +232,32 @@ func (h *jsonHook) PostEphemeralOp(id terraform.HookResourceIdentity, action pla
|
|||
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PreListQuery(id terraform.HookResourceIdentity, input_config cty.Value) (terraform.HookAction, error) {
|
||||
addr := id.Addr
|
||||
h.view.log.Info(
|
||||
fmt.Sprintf("%s: Starting query...", addr.String()),
|
||||
"type", json.MessageListStart,
|
||||
json.MessageListStart, json.NewQueryStart(addr, input_config),
|
||||
)
|
||||
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PostListQuery(id terraform.HookResourceIdentity, results plans.QueryResults) (terraform.HookAction, error) {
|
||||
addr := id.Addr
|
||||
data := results.Value.GetAttr("data")
|
||||
for it := data.ElementIterator(); it.Next(); {
|
||||
_, value := it.Element()
|
||||
|
||||
result := json.NewQueryResult(addr, value)
|
||||
|
||||
h.view.log.Info(
|
||||
fmt.Sprintf("%s: Result found", addr.String()),
|
||||
"type", json.MessageListResourceFound,
|
||||
json.MessageListResourceFound, result,
|
||||
)
|
||||
}
|
||||
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -507,6 +507,28 @@ func (h *UiHook) PostEphemeralOp(rId terraform.HookResourceIdentity, action plan
|
|||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *UiHook) PreListQuery(id terraform.HookResourceIdentity, input_config cty.Value) (terraform.HookAction, error) {
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *UiHook) PostListQuery(id terraform.HookResourceIdentity, results plans.QueryResults) (terraform.HookAction, error) {
|
||||
addr := id.Addr
|
||||
data := results.Value.GetAttr("data")
|
||||
for it := data.ElementIterator(); it.Next(); {
|
||||
_, value := it.Element()
|
||||
|
||||
h.println(fmt.Sprintf(
|
||||
"%s\t%s\t%s",
|
||||
addr.String(),
|
||||
// TODO maybe deduplicate common identity attributes?
|
||||
tfdiags.ObjectToString(value.GetAttr("identity")),
|
||||
value.GetAttr("display_name").AsString(),
|
||||
))
|
||||
}
|
||||
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
// Wrap calls to the view so that concurrent calls do not interleave println.
|
||||
func (h *UiHook) println(s string) {
|
||||
h.viewLock.Lock()
|
||||
|
|
|
|||
|
|
@ -46,4 +46,8 @@ const (
|
|||
MessageTestInterrupt MessageType = "test_interrupt"
|
||||
MessageTestStatus MessageType = "test_status"
|
||||
MessageTestRetry MessageType = "test_retry"
|
||||
|
||||
// List messages
|
||||
MessageListStart MessageType = "list_start"
|
||||
MessageListResourceFound MessageType = "list_resource_found"
|
||||
)
|
||||
|
|
|
|||
61
internal/command/views/json/query.go
Normal file
61
internal/command/views/json/query.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
)
|
||||
|
||||
type QueryStart struct {
|
||||
Address string `json:"address"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
InputConfig map[string]json.RawMessage `json:"input_config,omitempty"`
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
Address string `json:"address"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Identity map[string]json.RawMessage `json:"identity"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceObject map[string]json.RawMessage `json:"resource_object,omitempty"`
|
||||
Config string `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
func NewQueryStart(addr addrs.AbsResourceInstance, input_config cty.Value) QueryStart {
|
||||
return QueryStart{
|
||||
Address: addr.String(),
|
||||
ResourceType: addr.Resource.Resource.Type,
|
||||
InputConfig: marshalValues(input_config),
|
||||
}
|
||||
}
|
||||
|
||||
func NewQueryResult(addr addrs.AbsResourceInstance, value cty.Value) QueryResult {
|
||||
return QueryResult{
|
||||
Address: addr.String(),
|
||||
DisplayName: value.GetAttr("display_name").AsString(),
|
||||
Identity: marshalValues(value.GetAttr("identity")),
|
||||
ResourceType: addr.Resource.Resource.Type,
|
||||
ResourceObject: marshalValues(value.GetAttr("state")),
|
||||
// TODO: Add config once we have it available
|
||||
}
|
||||
}
|
||||
|
||||
func marshalValues(value cty.Value) map[string]json.RawMessage {
|
||||
if value == cty.NilVal || value.IsNull() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(map[string]json.RawMessage)
|
||||
it := value.ElementIterator()
|
||||
for it.Next() {
|
||||
k, v := it.Element()
|
||||
vJSON, _ := ctyjson.Marshal(v, v.Type())
|
||||
ret[k.AsString()] = json.RawMessage(vJSON)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
85
internal/command/views/query.go
Normal file
85
internal/command/views/query.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Query renders outputs for query executions.
|
||||
type Query interface {
|
||||
Operation() Operation
|
||||
Hooks() []terraform.Hook
|
||||
|
||||
Diagnostics(diags tfdiags.Diagnostics)
|
||||
HelpPrompt()
|
||||
}
|
||||
|
||||
func NewQuery(vt arguments.ViewType, view *View) Query {
|
||||
switch vt {
|
||||
case arguments.ViewJSON:
|
||||
return &QueryJSON{
|
||||
view: NewJSONView(view),
|
||||
}
|
||||
case arguments.ViewHuman:
|
||||
return &QueryHuman{
|
||||
view: view,
|
||||
inAutomation: view.RunningInAutomation(),
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown view type %v", vt))
|
||||
}
|
||||
}
|
||||
|
||||
type QueryHuman struct {
|
||||
view *View
|
||||
|
||||
inAutomation bool
|
||||
}
|
||||
|
||||
var _ Query = (*QueryHuman)(nil)
|
||||
|
||||
func (v *QueryHuman) Operation() Operation {
|
||||
return NewQueryOperation(arguments.ViewHuman, v.inAutomation, v.view)
|
||||
}
|
||||
|
||||
func (v *QueryHuman) Hooks() []terraform.Hook {
|
||||
return []terraform.Hook{
|
||||
NewUiHook(v.view),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *QueryHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
func (v *QueryHuman) HelpPrompt() {
|
||||
v.view.HelpPrompt("query")
|
||||
}
|
||||
|
||||
type QueryJSON struct {
|
||||
view *JSONView
|
||||
}
|
||||
|
||||
var _ Query = (*QueryJSON)(nil)
|
||||
|
||||
func (v *QueryJSON) Operation() Operation {
|
||||
return &QueryOperationJSON{view: v.view}
|
||||
}
|
||||
|
||||
func (v *QueryJSON) Hooks() []terraform.Hook {
|
||||
return []terraform.Hook{
|
||||
newJSONHook(v.view),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *QueryJSON) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
func (v *QueryJSON) HelpPrompt() {
|
||||
}
|
||||
111
internal/command/views/query_operation.go
Normal file
111
internal/command/views/query_operation.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func NewQueryOperation(vt arguments.ViewType, inAutomation bool, view *View) Operation {
|
||||
switch vt {
|
||||
case arguments.ViewHuman:
|
||||
return &QueryOperationHuman{view: view, inAutomation: inAutomation}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown view type %v", vt))
|
||||
}
|
||||
}
|
||||
|
||||
type QueryOperationHuman struct {
|
||||
view *View
|
||||
|
||||
// inAutomation indicates that commands are being run by an
|
||||
// automated system rather than directly at a command prompt.
|
||||
//
|
||||
// This is a hint not to produce messages that expect that a user can
|
||||
// run a follow-up command, perhaps because Terraform is running in
|
||||
// some sort of workflow automation tool that abstracts away the
|
||||
// exact commands that are being run.
|
||||
inAutomation bool
|
||||
}
|
||||
|
||||
var _ Operation = (*QueryOperationHuman)(nil)
|
||||
|
||||
func (v *QueryOperationHuman) Interrupted() {
|
||||
v.view.streams.Println(format.WordWrap(interrupted, v.view.outputColumns()))
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) FatalInterrupt() {
|
||||
v.view.streams.Eprintln(format.WordWrap(fatalInterrupt, v.view.errorColumns()))
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) Stopping() {
|
||||
v.view.streams.Println("Stopping operation...")
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) Cancelled(planMode plans.Mode) {
|
||||
v.view.streams.Println("Query cancelled.")
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) EmergencyDumpState(stateFile *statefile.File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) PlanNextStep(planPath string, genConfigPath string) {
|
||||
}
|
||||
|
||||
func (v *QueryOperationHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
type QueryOperationJSON struct {
|
||||
view *JSONView
|
||||
}
|
||||
|
||||
var _ Operation = (*QueryOperationJSON)(nil)
|
||||
|
||||
func (v *QueryOperationJSON) Interrupted() {
|
||||
v.view.Log(interrupted)
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) FatalInterrupt() {
|
||||
v.view.Log(fatalInterrupt)
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) Stopping() {
|
||||
v.view.Log("Stopping operation...")
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) Cancelled(planMode plans.Mode) {
|
||||
v.view.Log("Query cancelled")
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) PlanNextStep(planPath string, genConfigPath string) {
|
||||
}
|
||||
|
||||
func (v *QueryOperationJSON) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
|
@ -22,6 +22,8 @@ type Changes struct {
|
|||
// Resources tracks planned changes to resource instance objects.
|
||||
Resources []*ResourceInstanceChange
|
||||
|
||||
Queries []*QueryInstance
|
||||
|
||||
// Outputs tracks planned changes output values.
|
||||
//
|
||||
// Note that although an in-memory plan contains planned changes for
|
||||
|
|
@ -73,6 +75,24 @@ func (c *Changes) Encode(schemas *schemarepo.Schemas) (*ChangesSrc, error) {
|
|||
changesSrc.Resources = append(changesSrc.Resources, rcs)
|
||||
}
|
||||
|
||||
for _, qi := range c.Queries {
|
||||
p, ok := schemas.Providers[qi.ProviderAddr.Provider]
|
||||
if !ok {
|
||||
return changesSrc, fmt.Errorf("Changes.Encode: missing provider %s for %s", qi.ProviderAddr, qi.Addr)
|
||||
}
|
||||
|
||||
schema := p.ListResourceTypes[qi.Addr.Resource.Resource.Type]
|
||||
if schema.Body == nil {
|
||||
return changesSrc, fmt.Errorf("Changes.Encode: missing schema for %s", qi.Addr)
|
||||
}
|
||||
rcs, err := qi.Encode(schema)
|
||||
if err != nil {
|
||||
return changesSrc, fmt.Errorf("Changes.Encode: %w", err)
|
||||
}
|
||||
|
||||
changesSrc.Queries = append(changesSrc.Queries, rcs)
|
||||
}
|
||||
|
||||
for _, ocs := range c.Outputs {
|
||||
oc, err := ocs.Encode()
|
||||
if err != nil {
|
||||
|
|
@ -113,6 +133,18 @@ func (c *Changes) InstancesForAbsResource(addr addrs.AbsResource) []*ResourceIns
|
|||
return changes
|
||||
}
|
||||
|
||||
func (c *Changes) QueriesForAbsResource(addr addrs.AbsResource) []*QueryInstance {
|
||||
var queries []*QueryInstance
|
||||
for _, q := range c.Queries {
|
||||
qAddr := q.Addr.ContainingResource()
|
||||
if qAddr.Equal(addr) {
|
||||
queries = append(queries, q)
|
||||
}
|
||||
}
|
||||
|
||||
return queries
|
||||
}
|
||||
|
||||
// InstancesForConfigResource returns the planned change for the current objects
|
||||
// of the resource instances of the given address, if any. Returns nil if no
|
||||
// changes are planned.
|
||||
|
|
@ -210,6 +242,40 @@ func (c *Changes) SyncWrapper() *ChangesSync {
|
|||
}
|
||||
}
|
||||
|
||||
type QueryInstance struct {
|
||||
Addr addrs.AbsResourceInstance
|
||||
|
||||
ProviderAddr addrs.AbsProviderConfig
|
||||
|
||||
Results QueryResults
|
||||
}
|
||||
|
||||
type QueryResults struct {
|
||||
Value cty.Value
|
||||
}
|
||||
|
||||
func (qi *QueryInstance) DeepCopy() *QueryInstance {
|
||||
if qi == nil {
|
||||
return qi
|
||||
}
|
||||
|
||||
ret := *qi
|
||||
return &ret
|
||||
}
|
||||
|
||||
func (rc *QueryInstance) Encode(schema providers.Schema) (*QueryInstanceSrc, error) {
|
||||
results, err := NewDynamicValue(rc.Results.Value, schema.Body.ImpliedType())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &QueryInstanceSrc{
|
||||
Addr: rc.Addr,
|
||||
Results: results,
|
||||
ProviderAddr: rc.ProviderAddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResourceInstanceChange describes a change to a particular resource instance
|
||||
// object.
|
||||
type ResourceInstanceChange struct {
|
||||
|
|
@ -293,7 +359,6 @@ func (rc *ResourceInstanceChange) Encode(schema providers.Schema) (*ResourceInst
|
|||
prevRunAddr = rc.Addr
|
||||
}
|
||||
return &ResourceInstanceChangeSrc{
|
||||
|
||||
Addr: rc.Addr,
|
||||
PrevRunAddr: prevRunAddr,
|
||||
DeposedKey: rc.DeposedKey,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ type ChangesSrc struct {
|
|||
// Resources tracks planned changes to resource instance objects.
|
||||
Resources []*ResourceInstanceChangeSrc
|
||||
|
||||
Queries []*QueryInstanceSrc
|
||||
|
||||
// Outputs tracks planned changes output values.
|
||||
//
|
||||
// Note that although an in-memory plan contains planned changes for
|
||||
|
|
@ -114,8 +116,6 @@ func (c *ChangesSrc) Decode(schemas *schemarepo.Schemas) (*Changes, error) {
|
|||
schema = p.ResourceTypes[rcs.Addr.Resource.Resource.Type]
|
||||
case addrs.DataResourceMode:
|
||||
schema = p.DataSources[rcs.Addr.Resource.Resource.Type]
|
||||
case addrs.ListResourceMode:
|
||||
schema = p.ListResourceTypes[rcs.Addr.Resource.Resource.Type]
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected resource mode %s", rcs.Addr.Resource.Resource.Mode))
|
||||
}
|
||||
|
|
@ -135,6 +135,25 @@ func (c *ChangesSrc) Decode(schemas *schemarepo.Schemas) (*Changes, error) {
|
|||
changes.Resources = append(changes.Resources, rc)
|
||||
}
|
||||
|
||||
for _, qis := range c.Queries {
|
||||
p, ok := schemas.Providers[qis.ProviderAddr.Provider]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ChangesSrc.Decode: missing provider %s for %s", qis.ProviderAddr, qis.Addr)
|
||||
}
|
||||
schema := p.ListResourceTypes[qis.Addr.Resource.Resource.Type]
|
||||
|
||||
if schema.Body == nil {
|
||||
return nil, fmt.Errorf("ChangesSrc.Decode: missing schema for %s", qis.Addr)
|
||||
}
|
||||
|
||||
query, err := qis.Decode(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changes.Queries = append(changes.Queries, query)
|
||||
}
|
||||
|
||||
for _, ocs := range c.Outputs {
|
||||
oc, err := ocs.Decode()
|
||||
if err != nil {
|
||||
|
|
@ -156,6 +175,29 @@ func (c *ChangesSrc) AppendResourceInstanceChange(change *ResourceInstanceChange
|
|||
c.Resources = append(c.Resources, s)
|
||||
}
|
||||
|
||||
type QueryInstanceSrc struct {
|
||||
Addr addrs.AbsResourceInstance
|
||||
|
||||
ProviderAddr addrs.AbsProviderConfig
|
||||
|
||||
Results DynamicValue
|
||||
}
|
||||
|
||||
func (qis *QueryInstanceSrc) Decode(schema providers.Schema) (*QueryInstance, error) {
|
||||
query, err := qis.Results.Decode(schema.Body.ImpliedType())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &QueryInstance{
|
||||
Addr: qis.Addr,
|
||||
Results: QueryResults{
|
||||
Value: query,
|
||||
},
|
||||
ProviderAddr: qis.ProviderAddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResourceInstanceChangeSrc is a not-yet-decoded ResourceInstanceChange.
|
||||
// Pass the associated resource type's schema type to method Decode to
|
||||
// obtain a ResourceInstanceChange.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,17 @@ func (cs *ChangesSync) AppendResourceInstanceChange(change *ResourceInstanceChan
|
|||
cs.changes.Resources = append(cs.changes.Resources, s)
|
||||
}
|
||||
|
||||
func (cs *ChangesSync) AppendQueryInstance(query *QueryInstance) {
|
||||
if cs == nil {
|
||||
panic("AppendQueryInstance on nil ChangesSync")
|
||||
}
|
||||
cs.lock.Lock()
|
||||
defer cs.lock.Unlock()
|
||||
|
||||
s := query.DeepCopy() // TODO do we need to deep copy here?
|
||||
cs.changes.Queries = append(cs.changes.Queries, s)
|
||||
}
|
||||
|
||||
// GetResourceInstanceChange searches the set of resource instance changes for
|
||||
// one matching the given address and deposed key, returning it if it exists.
|
||||
// Use [addrs.NotDeposed] as the deposed key to represent the "current"
|
||||
|
|
@ -106,6 +117,19 @@ func (cs *ChangesSync) GetChangesForAbsResource(addr addrs.AbsResource) []*Resou
|
|||
return changes
|
||||
}
|
||||
|
||||
func (cs *ChangesSync) GetQueryInstancesForAbsResource(addr addrs.AbsResource) []*QueryInstance {
|
||||
if cs == nil {
|
||||
panic("GetQueryInstancesForAbsResource on nil ChangesSync")
|
||||
}
|
||||
cs.lock.Lock()
|
||||
defer cs.lock.Unlock()
|
||||
var queries []*QueryInstance
|
||||
for _, q := range cs.changes.QueriesForAbsResource(addr) {
|
||||
queries = append(queries, q.DeepCopy())
|
||||
}
|
||||
return queries
|
||||
}
|
||||
|
||||
// RemoveResourceInstanceChange searches the set of resource instance changes
|
||||
// for one matching the given address and deposed key, and removes it from the
|
||||
// set if it exists.
|
||||
|
|
|
|||
|
|
@ -112,20 +112,16 @@ func TestContext2Plan_queryList(t *testing.T) {
|
|||
"list.test_resource.test2": {},
|
||||
}
|
||||
actualResources := map[string][]string{}
|
||||
for _, change := range changes.Resources {
|
||||
for _, change := range changes.Queries {
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
obj := cs.Results.Value.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
|
|
@ -229,7 +225,7 @@ func TestContext2Plan_queryList(t *testing.T) {
|
|||
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
|
||||
expectedResources := []string{"list.test_resource.test[0]", "list.test_resource.test2"}
|
||||
actualResources := make([]string, 0)
|
||||
for _, change := range changes.Resources {
|
||||
for _, change := range changes.Queries {
|
||||
actualResources = append(actualResources, change.Addr.String())
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
|
|
@ -237,14 +233,10 @@ func TestContext2Plan_queryList(t *testing.T) {
|
|||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
expectedTypes := []string{"ami-123456", "ami-654321"}
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
obj := cs.Results.Value.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
|
|
@ -535,7 +527,7 @@ func TestContext2Plan_queryList(t *testing.T) {
|
|||
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
|
||||
expectedResources := []string{"list.test_resource.test1", "list.test_resource.test2"}
|
||||
actualResources := make([]string, 0)
|
||||
for _, change := range changes.Resources {
|
||||
for _, change := range changes.Queries {
|
||||
actualResources = append(actualResources, change.Addr.String())
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
|
|
@ -543,14 +535,10 @@ func TestContext2Plan_queryList(t *testing.T) {
|
|||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
expectedTypes := []string{"ami-123456"}
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
obj := cs.Results.Value.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
|
|
@ -650,7 +638,7 @@ func TestContext2Plan_queryList(t *testing.T) {
|
|||
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
|
||||
expectedResources := []string{"list.test_resource.test1[\"foo\"]", "list.test_resource.test1[\"bar\"]", "list.test_resource.test2[\"foo\"]", "list.test_resource.test2[\"bar\"]"}
|
||||
actualResources := make([]string, 0)
|
||||
for _, change := range changes.Resources {
|
||||
for _, change := range changes.Queries {
|
||||
actualResources = append(actualResources, change.Addr.String())
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
|
|
@ -658,14 +646,10 @@ func TestContext2Plan_queryList(t *testing.T) {
|
|||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
expectedTypes := []string{"ami-123456"}
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
obj := cs.Results.Value.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -846,9 +846,9 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
return cty.DynamicVal, diags
|
||||
}
|
||||
resourceType := resourceSchema.Body.ImpliedType()
|
||||
changes := d.Evaluator.Changes.GetChangesForAbsResource(lAddr.Absolute(d.ModulePath))
|
||||
queries := d.Evaluator.Changes.GetQueryInstancesForAbsResource(lAddr.Absolute(d.ModulePath))
|
||||
|
||||
if len(changes) == 0 {
|
||||
if len(queries) == 0 {
|
||||
// Since we know there are no instances, return an empty container of the expected type.
|
||||
switch {
|
||||
case config.Count != nil:
|
||||
|
|
@ -865,7 +865,7 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
case config.Count != nil:
|
||||
// figure out what the last index we have is
|
||||
length := -1
|
||||
for _, inst := range changes {
|
||||
for _, inst := range queries {
|
||||
if intKey, ok := inst.Addr.Resource.Key.(addrs.IntKey); ok {
|
||||
length = max(int(intKey)+1, length)
|
||||
}
|
||||
|
|
@ -873,10 +873,10 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
|
||||
if length > 0 {
|
||||
vals := make([]cty.Value, length)
|
||||
for _, inst := range changes {
|
||||
for _, inst := range queries {
|
||||
key := inst.Addr.Resource.Key
|
||||
if intKey, ok := key.(addrs.IntKey); ok {
|
||||
vals[int(intKey)] = inst.After
|
||||
vals[int(intKey)] = inst.Results.Value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -892,10 +892,10 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
}
|
||||
case config.ForEach != nil:
|
||||
vals := make(map[string]cty.Value)
|
||||
for _, inst := range changes {
|
||||
for _, inst := range queries {
|
||||
key := inst.Addr.Resource.Key
|
||||
if strKey, ok := key.(addrs.StringKey); ok {
|
||||
vals[string(strKey)] = inst.After
|
||||
vals[string(strKey)] = inst.Results.Value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -909,13 +909,13 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
ret = cty.EmptyObjectVal
|
||||
}
|
||||
default:
|
||||
if len(changes) <= 0 {
|
||||
if len(queries) <= 0 {
|
||||
// if the instance is missing, insert an empty tuple
|
||||
ret = cty.ObjectVal(map[string]cty.Value{
|
||||
"data": cty.EmptyTupleVal,
|
||||
})
|
||||
} else {
|
||||
ret = changes[0].After
|
||||
ret = queries[0].Results.Value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,11 @@ type Hook interface {
|
|||
PreEphemeralOp(id HookResourceIdentity, action plans.Action) (HookAction, error)
|
||||
PostEphemeralOp(id HookResourceIdentity, action plans.Action, opErr error) (HookAction, error)
|
||||
|
||||
// PreListQuery and PostListQuery are called during a query operation before and after
|
||||
// resources are queried from the provider.
|
||||
PreListQuery(id HookResourceIdentity, input_config cty.Value) (HookAction, error)
|
||||
PostListQuery(id HookResourceIdentity, results plans.QueryResults) (HookAction, error)
|
||||
|
||||
// Stopping is called if an external signal requests that Terraform
|
||||
// gracefully abort an operation in progress.
|
||||
//
|
||||
|
|
@ -209,6 +214,14 @@ func (h *NilHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action,
|
|||
return HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *NilHook) PreListQuery(id HookResourceIdentity, input_config cty.Value) (HookAction, error) {
|
||||
return HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *NilHook) PostListQuery(id HookResourceIdentity, results plans.QueryResults) (HookAction, error) {
|
||||
return HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (*NilHook) Stopping() {
|
||||
// Does nothing at all by default
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,16 @@ type MockHook struct {
|
|||
PostEphemeralOpReturn HookAction
|
||||
PostEphemeralOpReturnError error
|
||||
|
||||
PreListQueryCalled bool
|
||||
PreListQueryAddr addrs.AbsResourceInstance
|
||||
PreListQueryReturn HookAction
|
||||
PreListQueryReturnError error
|
||||
|
||||
PostListQueryCalled bool
|
||||
PostListQueryAddr addrs.AbsResourceInstance
|
||||
PostListQueryReturn HookAction
|
||||
PostListQueryReturnError error
|
||||
|
||||
StoppingCalled bool
|
||||
|
||||
PostStateUpdateCalled bool
|
||||
|
|
@ -346,6 +356,24 @@ func (h *MockHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action,
|
|||
return h.PostEphemeralOpReturn, h.PostEphemeralOpReturnError
|
||||
}
|
||||
|
||||
func (h *MockHook) PreListQuery(id HookResourceIdentity, input_config cty.Value) (HookAction, error) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
h.PreListQueryCalled = true
|
||||
h.PreListQueryAddr = id.Addr
|
||||
return h.PreListQueryReturn, h.PreListQueryReturnError
|
||||
}
|
||||
|
||||
func (h *MockHook) PostListQuery(id HookResourceIdentity, results plans.QueryResults) (HookAction, error) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
h.PostListQueryCalled = true
|
||||
h.PostListQueryAddr = id.Addr
|
||||
return h.PostListQueryReturn, h.PostListQueryReturnError
|
||||
}
|
||||
|
||||
func (h *MockHook) Stopping() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
|
|
|||
|
|
@ -98,6 +98,14 @@ func (h *stopHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action,
|
|||
return h.hook()
|
||||
}
|
||||
|
||||
func (h *stopHook) PreListQuery(id HookResourceIdentity, input_config cty.Value) (HookAction, error) {
|
||||
return h.hook()
|
||||
}
|
||||
|
||||
func (h *stopHook) PostListQuery(id HookResourceIdentity, results plans.QueryResults) (HookAction, error) {
|
||||
return h.hook()
|
||||
}
|
||||
|
||||
func (h *stopHook) Stopping() {}
|
||||
|
||||
func (h *stopHook) PostStateUpdate(new *states.State) (HookAction, error) {
|
||||
|
|
|
|||
|
|
@ -169,6 +169,20 @@ func (h *testHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action,
|
|||
return HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *testHook) PreListQuery(id HookResourceIdentity, input_config cty.Value) (HookAction, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.Calls = append(h.Calls, &testHookCall{"PreListQuery", id.Addr.String()})
|
||||
return HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *testHook) PostListQuery(id HookResourceIdentity, results plans.QueryResults) (HookAction, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.Calls = append(h.Calls, &testHookCall{"PostListQuery", id.Addr.String()})
|
||||
return HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *testHook) Stopping() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) {
|
||||
|
|
@ -76,6 +74,13 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di
|
|||
return diags
|
||||
}
|
||||
|
||||
rId := HookResourceIdentity{
|
||||
Addr: addr,
|
||||
ProviderAddr: n.ResolvedProvider.Provider,
|
||||
}
|
||||
ctx.Hook(func(h Hook) (HookAction, error) {
|
||||
return h.PreListQuery(rId, unmarkedBlockVal.GetAttr("config"))
|
||||
})
|
||||
// If we get down here then our configuration is complete and we're ready
|
||||
// to actually call the provider to list the data.
|
||||
resp := provider.ListResource(providers.ListResourceRequest{
|
||||
|
|
@ -84,21 +89,23 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di
|
|||
Limit: limit,
|
||||
IncludeResourceObject: includeResource,
|
||||
})
|
||||
if resp.Diagnostics != nil {
|
||||
return diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
||||
results := plans.QueryResults{
|
||||
Value: resp.Result,
|
||||
}
|
||||
ctx.Hook(func(h Hook) (HookAction, error) {
|
||||
return h.PostListQuery(rId, results)
|
||||
})
|
||||
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
change := &plans.ResourceInstanceChange{
|
||||
query := &plans.QueryInstance{
|
||||
Addr: n.Addr,
|
||||
ProviderAddr: n.ResolvedProvider,
|
||||
Change: plans.Change{
|
||||
Action: plans.Read,
|
||||
Before: cty.DynamicVal,
|
||||
After: resp.Result,
|
||||
},
|
||||
DeposedKey: states.NotDeposed,
|
||||
Results: results,
|
||||
}
|
||||
|
||||
ctx.Changes().AppendResourceInstanceChange(change)
|
||||
ctx.Changes().AppendQueryInstance(query)
|
||||
return diags
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue