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 }