mirror of https://github.com/hashicorp/terraform
Normally, `terraform output` refreshes and reads the entire state in the command package before pulling output values out of it. This doesn't give Terraform Cloud the opportunity to apply the read state outputs org permission and instead applies the read state versions permission. I decided to expand the state manager interface to provide a separate GetRootOutputValues function in order to give the cloud backend a more nuanced opportunity to fetch just the outputs. This required moving state Refresh/Read code that was previously in the command into the shared backend state as well as the filesystem state packages.pull/31507/head
parent
5da30c2b65
commit
c33c8b013f
@ -0,0 +1,110 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/states/remote"
|
||||
"github.com/hashicorp/terraform/internal/states/statemgr"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
// State is similar to remote State and delegates to it, except in the case of output values,
|
||||
// which use a separate methodology that ensures the caller is authorized to read cloud
|
||||
// workspace outputs.
|
||||
type State struct {
|
||||
Client *remoteClient
|
||||
|
||||
delegate remote.State
|
||||
}
|
||||
|
||||
// Proof that cloud State is a statemgr.Persistent interface
|
||||
var _ statemgr.Persistent = (*State)(nil)
|
||||
|
||||
func NewState(client *remoteClient) *State {
|
||||
return &State{
|
||||
Client: client,
|
||||
delegate: remote.State{Client: client},
|
||||
}
|
||||
}
|
||||
|
||||
// State delegates calls to read State to the remote State
|
||||
func (s *State) State() *states.State {
|
||||
return s.delegate.State()
|
||||
}
|
||||
|
||||
// Lock delegates calls to lock state to the remote State
|
||||
func (s *State) Lock(info *statemgr.LockInfo) (string, error) {
|
||||
return s.delegate.Lock(info)
|
||||
}
|
||||
|
||||
// Unlock delegates calls to unlock state to the remote State
|
||||
func (s *State) Unlock(id string) error {
|
||||
return s.delegate.Unlock(id)
|
||||
}
|
||||
|
||||
// RefreshState delegates calls to refresh State to the remote State
|
||||
func (s *State) RefreshState() error {
|
||||
return s.delegate.RefreshState()
|
||||
}
|
||||
|
||||
// RefreshState delegates calls to refresh State to the remote State
|
||||
func (s *State) PersistState() error {
|
||||
return s.delegate.PersistState()
|
||||
}
|
||||
|
||||
// WriteState delegates calls to write State to the remote State
|
||||
func (s *State) WriteState(state *states.State) error {
|
||||
return s.delegate.WriteState(state)
|
||||
}
|
||||
|
||||
// GetRootOutputValues fetches output values from Terraform Cloud
|
||||
func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
so, err := s.Client.client.StateVersionOutputs.ReadCurrent(ctx, s.Client.workspace.ID)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read state version outputs: %w", err)
|
||||
}
|
||||
|
||||
result := make(map[string]*states.OutputValue)
|
||||
|
||||
for _, output := range so.Items {
|
||||
if output.Sensitive {
|
||||
// Since this is a sensitive value, the output must be requested explicitly in order to
|
||||
// read its value, which is assumed to be present by callers
|
||||
sensitiveOutput, err := s.Client.client.StateVersionOutputs.Read(ctx, output.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read state version output %s: %w", output.ID, err)
|
||||
}
|
||||
output.Value = sensitiveOutput.Value
|
||||
}
|
||||
|
||||
bufType, err := json.Marshal(output.DetailedType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not marshal output %s type: %w", output.ID, err)
|
||||
}
|
||||
|
||||
var ctype cty.Type
|
||||
err = ctype.UnmarshalJSON(bufType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not interpret output %s type: %w", output.ID, err)
|
||||
}
|
||||
|
||||
cval, err := gocty.ToCtyValue(output.Value, ctype)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not interpret value %v as type %s for output %s: %w", cval, ctype.FriendlyName(), output.ID, err)
|
||||
}
|
||||
|
||||
result[output.Name] = &states.OutputValue{
|
||||
Value: cval,
|
||||
Sensitive: output.Sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-tfe"
|
||||
"github.com/hashicorp/terraform/internal/states/statemgr"
|
||||
)
|
||||
|
||||
func TestState_impl(t *testing.T) {
|
||||
var _ statemgr.Reader = new(State)
|
||||
var _ statemgr.Writer = new(State)
|
||||
var _ statemgr.Persister = new(State)
|
||||
var _ statemgr.Refresher = new(State)
|
||||
var _ statemgr.OutputReader = new(State)
|
||||
var _ statemgr.Locker = new(State)
|
||||
}
|
||||
|
||||
type ExpectedOutput struct {
|
||||
Name string
|
||||
Sensitive bool
|
||||
IsNull bool
|
||||
}
|
||||
|
||||
func TestState_GetRootOutputValues(t *testing.T) {
|
||||
b, bCleanup := testBackendWithOutputs(t)
|
||||
defer bCleanup()
|
||||
|
||||
client := &remoteClient{
|
||||
client: b.client,
|
||||
workspace: &tfe.Workspace{
|
||||
ID: "ws-abcd",
|
||||
},
|
||||
}
|
||||
|
||||
state := NewState(client)
|
||||
outputs, err := state.GetRootOutputValues()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error returned from GetRootOutputValues: %s", err)
|
||||
}
|
||||
|
||||
cases := []ExpectedOutput{
|
||||
{
|
||||
Name: "sensitive_output",
|
||||
Sensitive: true,
|
||||
IsNull: false,
|
||||
},
|
||||
{
|
||||
Name: "nonsensitive_output",
|
||||
Sensitive: false,
|
||||
IsNull: false,
|
||||
},
|
||||
{
|
||||
Name: "object_output",
|
||||
Sensitive: false,
|
||||
IsNull: false,
|
||||
},
|
||||
{
|
||||
Name: "list_output",
|
||||
Sensitive: false,
|
||||
IsNull: false,
|
||||
},
|
||||
}
|
||||
|
||||
if len(outputs) != len(cases) {
|
||||
t.Errorf("Expected %d item but %d were returned", len(cases), len(outputs))
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
so, ok := outputs[testCase.Name]
|
||||
if !ok {
|
||||
t.Fatalf("Expected key %s but it was not found", testCase.Name)
|
||||
}
|
||||
if so.Value.IsNull() != testCase.IsNull {
|
||||
t.Errorf("Key %s does not match null expectation %v", testCase.Name, testCase.IsNull)
|
||||
}
|
||||
if so.Sensitive != testCase.Sensitive {
|
||||
t.Errorf("Key %s does not match sensitive expectation %v", testCase.Name, testCase.Sensitive)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue