github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/cloud/state.go (about)

     1  package cloud
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/go-tfe"
    12  	"github.com/zclconf/go-cty/cty"
    13  	"github.com/zclconf/go-cty/cty/gocty"
    14  
    15  	"github.com/eliastor/durgaform/internal/states"
    16  	"github.com/eliastor/durgaform/internal/states/remote"
    17  	"github.com/eliastor/durgaform/internal/states/statemgr"
    18  )
    19  
    20  // State is similar to remote State and delegates to it, except in the case of output values,
    21  // which use a separate methodology that ensures the caller is authorized to read cloud
    22  // workspace outputs.
    23  type State struct {
    24  	Client *remoteClient
    25  
    26  	delegate remote.State
    27  }
    28  
    29  var ErrStateVersionUnauthorizedUpgradeState = errors.New(strings.TrimSpace(`
    30  You are not authorized to read the full state version containing outputs.
    31  State versions created by durgaform v1.3.0 and newer do not require this level
    32  of authorization and therefore this error can usually be fixed by upgrading the
    33  remote state version.
    34  `))
    35  
    36  // Proof that cloud State is a statemgr.Persistent interface
    37  var _ statemgr.Persistent = (*State)(nil)
    38  
    39  func NewState(client *remoteClient) *State {
    40  	return &State{
    41  		Client:   client,
    42  		delegate: remote.State{Client: client},
    43  	}
    44  }
    45  
    46  // State delegates calls to read State to the remote State
    47  func (s *State) State() *states.State {
    48  	return s.delegate.State()
    49  }
    50  
    51  // Lock delegates calls to lock state to the remote State
    52  func (s *State) Lock(info *statemgr.LockInfo) (string, error) {
    53  	return s.delegate.Lock(info)
    54  }
    55  
    56  // Unlock delegates calls to unlock state to the remote State
    57  func (s *State) Unlock(id string) error {
    58  	return s.delegate.Unlock(id)
    59  }
    60  
    61  // RefreshState delegates calls to refresh State to the remote State
    62  func (s *State) RefreshState() error {
    63  	return s.delegate.RefreshState()
    64  }
    65  
    66  // RefreshState delegates calls to refresh State to the remote State
    67  func (s *State) PersistState() error {
    68  	return s.delegate.PersistState()
    69  }
    70  
    71  // WriteState delegates calls to write State to the remote State
    72  func (s *State) WriteState(state *states.State) error {
    73  	return s.delegate.WriteState(state)
    74  }
    75  
    76  func (s *State) fallbackReadOutputsFromFullState() (map[string]*states.OutputValue, error) {
    77  	log.Printf("[DEBUG] falling back to reading full state")
    78  
    79  	if err := s.RefreshState(); err != nil {
    80  		return nil, fmt.Errorf("failed to load state: %w", err)
    81  	}
    82  
    83  	state := s.State()
    84  	if state == nil {
    85  		// We know that there is supposed to be state (and this is not simply a new workspace
    86  		// without state) because the fallback is only invoked when outputs are present but
    87  		// detailed types are not available.
    88  		return nil, ErrStateVersionUnauthorizedUpgradeState
    89  	}
    90  
    91  	return state.RootModule().OutputValues, nil
    92  }
    93  
    94  // GetRootOutputValues fetches output values from Durgaform Cloud
    95  func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) {
    96  	ctx := context.Background()
    97  
    98  	so, err := s.Client.client.StateVersionOutputs.ReadCurrent(ctx, s.Client.workspace.ID)
    99  
   100  	if err != nil {
   101  		return nil, fmt.Errorf("could not read state version outputs: %w", err)
   102  	}
   103  
   104  	result := make(map[string]*states.OutputValue)
   105  
   106  	for _, output := range so.Items {
   107  		if output.DetailedType == nil {
   108  			// If there is no detailed type information available, this state was probably created
   109  			// with a version of durgaform < 1.3.0. In this case, we'll eject completely from this
   110  			// function and fall back to the old behavior of reading the entire state file, which
   111  			// requires a higher level of authorization.
   112  			return s.fallbackReadOutputsFromFullState()
   113  		}
   114  
   115  		if output.Sensitive {
   116  			// Since this is a sensitive value, the output must be requested explicitly in order to
   117  			// read its value, which is assumed to be present by callers
   118  			sensitiveOutput, err := s.Client.client.StateVersionOutputs.Read(ctx, output.ID)
   119  			if err != nil {
   120  				return nil, fmt.Errorf("could not read state version output %s: %w", output.ID, err)
   121  			}
   122  			output.Value = sensitiveOutput.Value
   123  		}
   124  
   125  		cval, err := tfeOutputToCtyValue(*output)
   126  		if err != nil {
   127  			return nil, fmt.Errorf("could not decode output %s (ID %s)", output.Name, output.ID)
   128  		}
   129  
   130  		result[output.Name] = &states.OutputValue{
   131  			Value:     cval,
   132  			Sensitive: output.Sensitive,
   133  		}
   134  	}
   135  
   136  	return result, nil
   137  }
   138  
   139  // tfeOutputToCtyValue decodes a combination of TFE output value and detailed-type to create a
   140  // cty value that is suitable for use in durgaform.
   141  func tfeOutputToCtyValue(output tfe.StateVersionOutput) (cty.Value, error) {
   142  	var result cty.Value
   143  	bufType, err := json.Marshal(output.DetailedType)
   144  	if err != nil {
   145  		return result, fmt.Errorf("could not marshal output %s type: %w", output.ID, err)
   146  	}
   147  
   148  	var ctype cty.Type
   149  	err = ctype.UnmarshalJSON(bufType)
   150  	if err != nil {
   151  		return result, fmt.Errorf("could not interpret output %s type: %w", output.ID, err)
   152  	}
   153  
   154  	result, err = gocty.ToCtyValue(output.Value, ctype)
   155  	if err != nil {
   156  		return result, fmt.Errorf("could not interpret value %v as type %s for output %s: %w", result, ctype.FriendlyName(), output.ID, err)
   157  	}
   158  
   159  	return result, nil
   160  }