github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/cloud/state.go (about) 1 package cloud 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/md5" 7 "encoding/base64" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "log" 12 "os" 13 "strings" 14 "sync" 15 16 "github.com/zclconf/go-cty/cty" 17 "github.com/zclconf/go-cty/cty/gocty" 18 19 tfe "github.com/hashicorp/go-tfe" 20 uuid "github.com/hashicorp/go-uuid" 21 "github.com/hashicorp/terraform/internal/command/jsonstate" 22 "github.com/hashicorp/terraform/internal/states" 23 "github.com/hashicorp/terraform/internal/states/remote" 24 "github.com/hashicorp/terraform/internal/states/statefile" 25 "github.com/hashicorp/terraform/internal/states/statemgr" 26 "github.com/hashicorp/terraform/internal/terraform" 27 ) 28 29 // State implements the State interfaces in the state package to handle 30 // reading and writing the remote state to TFC. This State on its own does no 31 // local caching so every persist will go to the remote storage and local 32 // writes will go to memory. 33 type State struct { 34 mu sync.Mutex 35 36 // We track two pieces of meta data in addition to the state itself: 37 // 38 // lineage - the state's unique ID 39 // serial - the monotonic counter of "versions" of the state 40 // 41 // Both of these (along with state) have a sister field 42 // that represents the values read in from an existing source. 43 // All three of these values are used to determine if the new 44 // state has changed from an existing state we read in. 45 lineage, readLineage string 46 serial, readSerial uint64 47 state, readState *states.State 48 disableLocks bool 49 tfeClient *tfe.Client 50 organization string 51 workspace *tfe.Workspace 52 stateUploadErr bool 53 forcePush bool 54 lockInfo *statemgr.LockInfo 55 } 56 57 var ErrStateVersionUnauthorizedUpgradeState = errors.New(strings.TrimSpace(` 58 You are not authorized to read the full state version containing outputs. 59 State versions created by terraform v1.3.0 and newer do not require this level 60 of authorization and therefore this error can usually be fixed by upgrading the 61 remote state version. 62 `)) 63 64 var _ statemgr.Full = (*State)(nil) 65 var _ statemgr.Migrator = (*State)(nil) 66 67 // statemgr.Reader impl. 68 func (s *State) State() *states.State { 69 s.mu.Lock() 70 defer s.mu.Unlock() 71 72 return s.state.DeepCopy() 73 } 74 75 // StateForMigration is part of our implementation of statemgr.Migrator. 76 func (s *State) StateForMigration() *statefile.File { 77 s.mu.Lock() 78 defer s.mu.Unlock() 79 80 return statefile.New(s.state.DeepCopy(), s.lineage, s.serial) 81 } 82 83 // WriteStateForMigration is part of our implementation of statemgr.Migrator. 84 func (s *State) WriteStateForMigration(f *statefile.File, force bool) error { 85 s.mu.Lock() 86 defer s.mu.Unlock() 87 88 if !force { 89 checkFile := statefile.New(s.state, s.lineage, s.serial) 90 if err := statemgr.CheckValidImport(f, checkFile); err != nil { 91 return err 92 } 93 } 94 95 // We create a deep copy of the state here, because the caller also has 96 // a reference to the given object and can potentially go on to mutate 97 // it after we return, but we want the snapshot at this point in time. 98 s.state = f.State.DeepCopy() 99 s.lineage = f.Lineage 100 s.serial = f.Serial 101 s.forcePush = force 102 103 return nil 104 } 105 106 // DisableLocks turns the Lock and Unlock methods into no-ops. This is intended 107 // to be called during initialization of a state manager and should not be 108 // called after any of the statemgr.Full interface methods have been called. 109 func (s *State) DisableLocks() { 110 s.disableLocks = true 111 } 112 113 // StateSnapshotMeta returns the metadata from the most recently persisted 114 // or refreshed persistent state snapshot. 115 // 116 // This is an implementation of statemgr.PersistentMeta. 117 func (s *State) StateSnapshotMeta() statemgr.SnapshotMeta { 118 return statemgr.SnapshotMeta{ 119 Lineage: s.lineage, 120 Serial: s.serial, 121 } 122 } 123 124 // statemgr.Writer impl. 125 func (s *State) WriteState(state *states.State) error { 126 s.mu.Lock() 127 defer s.mu.Unlock() 128 129 // We create a deep copy of the state here, because the caller also has 130 // a reference to the given object and can potentially go on to mutate 131 // it after we return, but we want the snapshot at this point in time. 132 s.state = state.DeepCopy() 133 s.forcePush = false 134 135 return nil 136 } 137 138 // PersistState uploads a snapshot of the latest state as a StateVersion to Terraform Cloud 139 func (s *State) PersistState(schemas *terraform.Schemas) error { 140 s.mu.Lock() 141 defer s.mu.Unlock() 142 143 if s.readState != nil { 144 lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage 145 serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial 146 stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState) 147 if stateUnchanged && lineageUnchanged && serialUnchanged { 148 // If the state, lineage or serial haven't changed at all then we have nothing to do. 149 return nil 150 } 151 s.serial++ 152 } else { 153 // We might be writing a new state altogether, but before we do that 154 // we'll check to make sure there isn't already a snapshot present 155 // that we ought to be updating. 156 err := s.refreshState() 157 if err != nil { 158 return fmt.Errorf("failed checking for existing remote state: %s", err) 159 } 160 if s.lineage == "" { // indicates that no state snapshot is present yet 161 lineage, err := uuid.GenerateUUID() 162 if err != nil { 163 return fmt.Errorf("failed to generate initial lineage: %v", err) 164 } 165 s.lineage = lineage 166 s.serial = 0 167 } 168 } 169 170 f := statefile.New(s.state, s.lineage, s.serial) 171 172 var buf bytes.Buffer 173 err := statefile.Write(f, &buf) 174 if err != nil { 175 return err 176 } 177 178 var jsonState []byte 179 if schemas != nil { 180 jsonState, err = jsonstate.Marshal(f, schemas) 181 if err != nil { 182 return err 183 } 184 } 185 186 stateFile, err := statefile.Read(bytes.NewReader(buf.Bytes())) 187 if err != nil { 188 return fmt.Errorf("failed to read state: %w", err) 189 } 190 191 ov, err := jsonstate.MarshalOutputs(stateFile.State.RootModule().OutputValues) 192 if err != nil { 193 return fmt.Errorf("failed to translate outputs: %w", err) 194 } 195 jsonStateOutputs, err := json.Marshal(ov) 196 if err != nil { 197 return fmt.Errorf("failed to marshal outputs to json: %w", err) 198 } 199 200 err = s.uploadState(s.lineage, s.serial, s.forcePush, buf.Bytes(), jsonState, jsonStateOutputs) 201 if err != nil { 202 s.stateUploadErr = true 203 return fmt.Errorf("error uploading state: %w", err) 204 } 205 // After we've successfully persisted, what we just wrote is our new 206 // reference state until someone calls RefreshState again. 207 // We've potentially overwritten (via force) the state, lineage 208 // and / or serial (and serial was incremented) so we copy over all 209 // three fields so everything matches the new state and a subsequent 210 // operation would correctly detect no changes to the lineage, serial or state. 211 s.readState = s.state.DeepCopy() 212 s.readLineage = s.lineage 213 s.readSerial = s.serial 214 return nil 215 } 216 217 func (s *State) uploadState(lineage string, serial uint64, isForcePush bool, state, jsonState, jsonStateOutputs []byte) error { 218 ctx := context.Background() 219 220 options := tfe.StateVersionCreateOptions{ 221 Lineage: tfe.String(lineage), 222 Serial: tfe.Int64(int64(serial)), 223 MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), 224 State: tfe.String(base64.StdEncoding.EncodeToString(state)), 225 Force: tfe.Bool(isForcePush), 226 JSONState: tfe.String(base64.StdEncoding.EncodeToString(jsonState)), 227 JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(jsonStateOutputs)), 228 } 229 230 // If we have a run ID, make sure to add it to the options 231 // so the state will be properly associated with the run. 232 runID := os.Getenv("TFE_RUN_ID") 233 if runID != "" { 234 options.Run = &tfe.Run{ID: runID} 235 } 236 // Create the new state. 237 _, err := s.tfeClient.StateVersions.Create(ctx, s.workspace.ID, options) 238 return err 239 } 240 241 // Lock calls the Client's Lock method if it's implemented. 242 func (s *State) Lock(info *statemgr.LockInfo) (string, error) { 243 s.mu.Lock() 244 defer s.mu.Unlock() 245 246 if s.disableLocks { 247 return "", nil 248 } 249 ctx := context.Background() 250 251 lockErr := &statemgr.LockError{Info: s.lockInfo} 252 253 // Lock the workspace. 254 _, err := s.tfeClient.Workspaces.Lock(ctx, s.workspace.ID, tfe.WorkspaceLockOptions{ 255 Reason: tfe.String("Locked by Terraform"), 256 }) 257 if err != nil { 258 if err == tfe.ErrWorkspaceLocked { 259 lockErr.Info = info 260 err = fmt.Errorf("%s (lock ID: \"%s/%s\")", err, s.organization, s.workspace.Name) 261 } 262 lockErr.Err = err 263 return "", lockErr 264 } 265 266 s.lockInfo = info 267 268 return s.lockInfo.ID, nil 269 } 270 271 // statemgr.Refresher impl. 272 func (s *State) RefreshState() error { 273 s.mu.Lock() 274 defer s.mu.Unlock() 275 return s.refreshState() 276 } 277 278 // refreshState is the main implementation of RefreshState, but split out so 279 // that we can make internal calls to it from methods that are already holding 280 // the s.mu lock. 281 func (s *State) refreshState() error { 282 payload, err := s.getStatePayload() 283 if err != nil { 284 return err 285 } 286 287 // no remote state is OK 288 if payload == nil { 289 s.readState = nil 290 s.lineage = "" 291 s.serial = 0 292 return nil 293 } 294 295 stateFile, err := statefile.Read(bytes.NewReader(payload.Data)) 296 if err != nil { 297 return err 298 } 299 300 s.lineage = stateFile.Lineage 301 s.serial = stateFile.Serial 302 s.state = stateFile.State 303 304 // Properties from the remote must be separate so we can 305 // track changes as lineage, serial and/or state are mutated 306 s.readLineage = stateFile.Lineage 307 s.readSerial = stateFile.Serial 308 s.readState = s.state.DeepCopy() 309 return nil 310 } 311 312 func (s *State) getStatePayload() (*remote.Payload, error) { 313 ctx := context.Background() 314 315 sv, err := s.tfeClient.StateVersions.ReadCurrent(ctx, s.workspace.ID) 316 if err != nil { 317 if err == tfe.ErrResourceNotFound { 318 // If no state exists, then return nil. 319 return nil, nil 320 } 321 return nil, fmt.Errorf("error retrieving state: %v", err) 322 } 323 324 state, err := s.tfeClient.StateVersions.Download(ctx, sv.DownloadURL) 325 if err != nil { 326 return nil, fmt.Errorf("error downloading state: %v", err) 327 } 328 329 // If the state is empty, then return nil. 330 if len(state) == 0 { 331 return nil, nil 332 } 333 334 // Get the MD5 checksum of the state. 335 sum := md5.Sum(state) 336 337 return &remote.Payload{ 338 Data: state, 339 MD5: sum[:], 340 }, nil 341 } 342 343 // Unlock calls the Client's Unlock method if it's implemented. 344 func (s *State) Unlock(id string) error { 345 s.mu.Lock() 346 defer s.mu.Unlock() 347 348 if s.disableLocks { 349 return nil 350 } 351 352 ctx := context.Background() 353 354 // We first check if there was an error while uploading the latest 355 // state. If so, we will not unlock the workspace to prevent any 356 // changes from being applied until the correct state is uploaded. 357 if s.stateUploadErr { 358 return nil 359 } 360 361 lockErr := &statemgr.LockError{Info: s.lockInfo} 362 363 // With lock info this should be treated as a normal unlock. 364 if s.lockInfo != nil { 365 // Verify the expected lock ID. 366 if s.lockInfo.ID != id { 367 lockErr.Err = fmt.Errorf("lock ID does not match existing lock") 368 return lockErr 369 } 370 371 // Unlock the workspace. 372 _, err := s.tfeClient.Workspaces.Unlock(ctx, s.workspace.ID) 373 if err != nil { 374 lockErr.Err = err 375 return lockErr 376 } 377 378 return nil 379 } 380 381 // Verify the optional force-unlock lock ID. 382 if s.organization+"/"+s.workspace.Name != id { 383 lockErr.Err = fmt.Errorf( 384 "lock ID %q does not match existing lock ID \"%s/%s\"", 385 id, 386 s.organization, 387 s.workspace.Name, 388 ) 389 return lockErr 390 } 391 392 // Force unlock the workspace. 393 _, err := s.tfeClient.Workspaces.ForceUnlock(ctx, s.workspace.ID) 394 if err != nil { 395 lockErr.Err = err 396 return lockErr 397 } 398 399 return nil 400 } 401 402 // Delete the remote state. 403 func (s *State) Delete() error { 404 err := s.tfeClient.Workspaces.Delete(context.Background(), s.organization, s.workspace.Name) 405 if err != nil && err != tfe.ErrResourceNotFound { 406 return fmt.Errorf("error deleting workspace %s: %v", s.workspace.Name, err) 407 } 408 409 return nil 410 } 411 412 // GetRootOutputValues fetches output values from Terraform Cloud 413 func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { 414 ctx := context.Background() 415 416 so, err := s.tfeClient.StateVersionOutputs.ReadCurrent(ctx, s.workspace.ID) 417 418 if err != nil { 419 return nil, fmt.Errorf("could not read state version outputs: %w", err) 420 } 421 422 result := make(map[string]*states.OutputValue) 423 424 for _, output := range so.Items { 425 if output.DetailedType == nil { 426 // If there is no detailed type information available, this state was probably created 427 // with a version of terraform < 1.3.0. In this case, we'll eject completely from this 428 // function and fall back to the old behavior of reading the entire state file, which 429 // requires a higher level of authorization. 430 log.Printf("[DEBUG] falling back to reading full state") 431 432 if err := s.RefreshState(); err != nil { 433 return nil, fmt.Errorf("failed to load state: %w", err) 434 } 435 436 state := s.State() 437 if state == nil { 438 // We know that there is supposed to be state (and this is not simply a new workspace 439 // without state) because the fallback is only invoked when outputs are present but 440 // detailed types are not available. 441 return nil, ErrStateVersionUnauthorizedUpgradeState 442 } 443 444 return state.RootModule().OutputValues, nil 445 } 446 447 if output.Sensitive { 448 // Since this is a sensitive value, the output must be requested explicitly in order to 449 // read its value, which is assumed to be present by callers 450 sensitiveOutput, err := s.tfeClient.StateVersionOutputs.Read(ctx, output.ID) 451 if err != nil { 452 return nil, fmt.Errorf("could not read state version output %s: %w", output.ID, err) 453 } 454 output.Value = sensitiveOutput.Value 455 } 456 457 cval, err := tfeOutputToCtyValue(*output) 458 if err != nil { 459 return nil, fmt.Errorf("could not decode output %s (ID %s)", output.Name, output.ID) 460 } 461 462 result[output.Name] = &states.OutputValue{ 463 Value: cval, 464 Sensitive: output.Sensitive, 465 } 466 } 467 468 return result, nil 469 } 470 471 // tfeOutputToCtyValue decodes a combination of TFE output value and detailed-type to create a 472 // cty value that is suitable for use in terraform. 473 func tfeOutputToCtyValue(output tfe.StateVersionOutput) (cty.Value, error) { 474 var result cty.Value 475 bufType, err := json.Marshal(output.DetailedType) 476 if err != nil { 477 return result, fmt.Errorf("could not marshal output %s type: %w", output.ID, err) 478 } 479 480 var ctype cty.Type 481 err = ctype.UnmarshalJSON(bufType) 482 if err != nil { 483 return result, fmt.Errorf("could not interpret output %s type: %w", output.ID, err) 484 } 485 486 result, err = gocty.ToCtyValue(output.Value, ctype) 487 if err != nil { 488 return result, fmt.Errorf("could not interpret value %v as type %s for output %s: %w", result, ctype.FriendlyName(), output.ID, err) 489 } 490 491 return result, nil 492 }