github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/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 log.Printf("[DEBUG] cloud/state: state read serial is: %d; serial is: %d", s.readSerial, s.serial) 144 log.Printf("[DEBUG] cloud/state: state read lineage is: %s; lineage is: %s", s.readLineage, s.lineage) 145 146 if s.readState != nil { 147 lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage 148 serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial 149 stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState) 150 if stateUnchanged && lineageUnchanged && serialUnchanged { 151 // If the state, lineage or serial haven't changed at all then we have nothing to do. 152 return nil 153 } 154 s.serial++ 155 } else { 156 // We might be writing a new state altogether, but before we do that 157 // we'll check to make sure there isn't already a snapshot present 158 // that we ought to be updating. 159 err := s.refreshState() 160 if err != nil { 161 return fmt.Errorf("failed checking for existing remote state: %s", err) 162 } 163 log.Printf("[DEBUG] cloud/state: after refresh, state read serial is: %d; serial is: %d", s.readSerial, s.serial) 164 log.Printf("[DEBUG] cloud/state: after refresh, state read lineage is: %s; lineage is: %s", s.readLineage, s.lineage) 165 166 if s.lineage == "" { // indicates that no state snapshot is present yet 167 lineage, err := uuid.GenerateUUID() 168 if err != nil { 169 return fmt.Errorf("failed to generate initial lineage: %v", err) 170 } 171 s.lineage = lineage 172 s.serial++ 173 } 174 } 175 176 f := statefile.New(s.state, s.lineage, s.serial) 177 178 var buf bytes.Buffer 179 err := statefile.Write(f, &buf) 180 if err != nil { 181 return err 182 } 183 184 var jsonState []byte 185 if schemas != nil { 186 jsonState, err = jsonstate.Marshal(f, schemas) 187 if err != nil { 188 return err 189 } 190 } 191 192 stateFile, err := statefile.Read(bytes.NewReader(buf.Bytes())) 193 if err != nil { 194 return fmt.Errorf("failed to read state: %w", err) 195 } 196 197 ov, err := jsonstate.MarshalOutputs(stateFile.State.RootModule().OutputValues) 198 if err != nil { 199 return fmt.Errorf("failed to translate outputs: %w", err) 200 } 201 jsonStateOutputs, err := json.Marshal(ov) 202 if err != nil { 203 return fmt.Errorf("failed to marshal outputs to json: %w", err) 204 } 205 206 err = s.uploadState(s.lineage, s.serial, s.forcePush, buf.Bytes(), jsonState, jsonStateOutputs) 207 if err != nil { 208 s.stateUploadErr = true 209 return fmt.Errorf("error uploading state: %w", err) 210 } 211 // After we've successfully persisted, what we just wrote is our new 212 // reference state until someone calls RefreshState again. 213 // We've potentially overwritten (via force) the state, lineage 214 // and / or serial (and serial was incremented) so we copy over all 215 // three fields so everything matches the new state and a subsequent 216 // operation would correctly detect no changes to the lineage, serial or state. 217 s.readState = s.state.DeepCopy() 218 s.readLineage = s.lineage 219 s.readSerial = s.serial 220 return nil 221 } 222 223 func (s *State) uploadState(lineage string, serial uint64, isForcePush bool, state, jsonState, jsonStateOutputs []byte) error { 224 ctx := context.Background() 225 226 options := tfe.StateVersionCreateOptions{ 227 Lineage: tfe.String(lineage), 228 Serial: tfe.Int64(int64(serial)), 229 MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), 230 State: tfe.String(base64.StdEncoding.EncodeToString(state)), 231 Force: tfe.Bool(isForcePush), 232 JSONState: tfe.String(base64.StdEncoding.EncodeToString(jsonState)), 233 JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(jsonStateOutputs)), 234 } 235 236 // If we have a run ID, make sure to add it to the options 237 // so the state will be properly associated with the run. 238 runID := os.Getenv("TFE_RUN_ID") 239 if runID != "" { 240 options.Run = &tfe.Run{ID: runID} 241 } 242 // Create the new state. 243 _, err := s.tfeClient.StateVersions.Create(ctx, s.workspace.ID, options) 244 return err 245 } 246 247 // Lock calls the Client's Lock method if it's implemented. 248 func (s *State) Lock(info *statemgr.LockInfo) (string, error) { 249 s.mu.Lock() 250 defer s.mu.Unlock() 251 252 if s.disableLocks { 253 return "", nil 254 } 255 ctx := context.Background() 256 257 lockErr := &statemgr.LockError{Info: s.lockInfo} 258 259 // Lock the workspace. 260 _, err := s.tfeClient.Workspaces.Lock(ctx, s.workspace.ID, tfe.WorkspaceLockOptions{ 261 Reason: tfe.String("Locked by Terraform"), 262 }) 263 if err != nil { 264 if err == tfe.ErrWorkspaceLocked { 265 lockErr.Info = info 266 err = fmt.Errorf("%s (lock ID: \"%s/%s\")", err, s.organization, s.workspace.Name) 267 } 268 lockErr.Err = err 269 return "", lockErr 270 } 271 272 s.lockInfo = info 273 274 return s.lockInfo.ID, nil 275 } 276 277 // statemgr.Refresher impl. 278 func (s *State) RefreshState() error { 279 s.mu.Lock() 280 defer s.mu.Unlock() 281 return s.refreshState() 282 } 283 284 // refreshState is the main implementation of RefreshState, but split out so 285 // that we can make internal calls to it from methods that are already holding 286 // the s.mu lock. 287 func (s *State) refreshState() error { 288 payload, err := s.getStatePayload() 289 if err != nil { 290 return err 291 } 292 293 // no remote state is OK 294 if payload == nil { 295 s.readState = nil 296 s.lineage = "" 297 s.serial = 0 298 return nil 299 } 300 301 stateFile, err := statefile.Read(bytes.NewReader(payload.Data)) 302 if err != nil { 303 return err 304 } 305 306 s.lineage = stateFile.Lineage 307 s.serial = stateFile.Serial 308 s.state = stateFile.State 309 310 // Properties from the remote must be separate so we can 311 // track changes as lineage, serial and/or state are mutated 312 s.readLineage = stateFile.Lineage 313 s.readSerial = stateFile.Serial 314 s.readState = s.state.DeepCopy() 315 return nil 316 } 317 318 func (s *State) getStatePayload() (*remote.Payload, error) { 319 ctx := context.Background() 320 321 sv, err := s.tfeClient.StateVersions.ReadCurrent(ctx, s.workspace.ID) 322 if err != nil { 323 if err == tfe.ErrResourceNotFound { 324 // If no state exists, then return nil. 325 return nil, nil 326 } 327 return nil, fmt.Errorf("error retrieving state: %v", err) 328 } 329 330 state, err := s.tfeClient.StateVersions.Download(ctx, sv.DownloadURL) 331 if err != nil { 332 return nil, fmt.Errorf("error downloading state: %v", err) 333 } 334 335 // If the state is empty, then return nil. 336 if len(state) == 0 { 337 return nil, nil 338 } 339 340 // Get the MD5 checksum of the state. 341 sum := md5.Sum(state) 342 343 return &remote.Payload{ 344 Data: state, 345 MD5: sum[:], 346 }, nil 347 } 348 349 // Unlock calls the Client's Unlock method if it's implemented. 350 func (s *State) Unlock(id string) error { 351 s.mu.Lock() 352 defer s.mu.Unlock() 353 354 if s.disableLocks { 355 return nil 356 } 357 358 ctx := context.Background() 359 360 // We first check if there was an error while uploading the latest 361 // state. If so, we will not unlock the workspace to prevent any 362 // changes from being applied until the correct state is uploaded. 363 if s.stateUploadErr { 364 return nil 365 } 366 367 lockErr := &statemgr.LockError{Info: s.lockInfo} 368 369 // With lock info this should be treated as a normal unlock. 370 if s.lockInfo != nil { 371 // Verify the expected lock ID. 372 if s.lockInfo.ID != id { 373 lockErr.Err = fmt.Errorf("lock ID does not match existing lock") 374 return lockErr 375 } 376 377 // Unlock the workspace. 378 _, err := s.tfeClient.Workspaces.Unlock(ctx, s.workspace.ID) 379 if err != nil { 380 lockErr.Err = err 381 return lockErr 382 } 383 384 return nil 385 } 386 387 // Verify the optional force-unlock lock ID. 388 if s.organization+"/"+s.workspace.Name != id { 389 lockErr.Err = fmt.Errorf( 390 "lock ID %q does not match existing lock ID \"%s/%s\"", 391 id, 392 s.organization, 393 s.workspace.Name, 394 ) 395 return lockErr 396 } 397 398 // Force unlock the workspace. 399 _, err := s.tfeClient.Workspaces.ForceUnlock(ctx, s.workspace.ID) 400 if err != nil { 401 lockErr.Err = err 402 return lockErr 403 } 404 405 return nil 406 } 407 408 // Delete the remote state. 409 func (s *State) Delete(force bool) error { 410 411 var err error 412 413 isSafeDeleteSupported := s.workspace.Permissions.CanForceDelete != nil 414 if force || !isSafeDeleteSupported { 415 err = s.tfeClient.Workspaces.Delete(context.Background(), s.organization, s.workspace.Name) 416 } else { 417 err = s.tfeClient.Workspaces.SafeDelete(context.Background(), s.organization, s.workspace.Name) 418 } 419 420 if err != nil && err != tfe.ErrResourceNotFound { 421 return fmt.Errorf("error deleting workspace %s: %v", s.workspace.Name, err) 422 } 423 424 return nil 425 } 426 427 // GetRootOutputValues fetches output values from Terraform Cloud 428 func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { 429 ctx := context.Background() 430 431 so, err := s.tfeClient.StateVersionOutputs.ReadCurrent(ctx, s.workspace.ID) 432 433 if err != nil { 434 return nil, fmt.Errorf("could not read state version outputs: %w", err) 435 } 436 437 result := make(map[string]*states.OutputValue) 438 439 for _, output := range so.Items { 440 if output.DetailedType == nil { 441 // If there is no detailed type information available, this state was probably created 442 // with a version of terraform < 1.3.0. In this case, we'll eject completely from this 443 // function and fall back to the old behavior of reading the entire state file, which 444 // requires a higher level of authorization. 445 log.Printf("[DEBUG] falling back to reading full state") 446 447 if err := s.RefreshState(); err != nil { 448 return nil, fmt.Errorf("failed to load state: %w", err) 449 } 450 451 state := s.State() 452 if state == nil { 453 // We know that there is supposed to be state (and this is not simply a new workspace 454 // without state) because the fallback is only invoked when outputs are present but 455 // detailed types are not available. 456 return nil, ErrStateVersionUnauthorizedUpgradeState 457 } 458 459 return state.RootModule().OutputValues, nil 460 } 461 462 if output.Sensitive { 463 // Since this is a sensitive value, the output must be requested explicitly in order to 464 // read its value, which is assumed to be present by callers 465 sensitiveOutput, err := s.tfeClient.StateVersionOutputs.Read(ctx, output.ID) 466 if err != nil { 467 return nil, fmt.Errorf("could not read state version output %s: %w", output.ID, err) 468 } 469 output.Value = sensitiveOutput.Value 470 } 471 472 cval, err := tfeOutputToCtyValue(*output) 473 if err != nil { 474 return nil, fmt.Errorf("could not decode output %s (ID %s)", output.Name, output.ID) 475 } 476 477 result[output.Name] = &states.OutputValue{ 478 Value: cval, 479 Sensitive: output.Sensitive, 480 } 481 } 482 483 return result, nil 484 } 485 486 // tfeOutputToCtyValue decodes a combination of TFE output value and detailed-type to create a 487 // cty value that is suitable for use in terraform. 488 func tfeOutputToCtyValue(output tfe.StateVersionOutput) (cty.Value, error) { 489 var result cty.Value 490 bufType, err := json.Marshal(output.DetailedType) 491 if err != nil { 492 return result, fmt.Errorf("could not marshal output %s type: %w", output.ID, err) 493 } 494 495 var ctype cty.Type 496 err = ctype.UnmarshalJSON(bufType) 497 if err != nil { 498 return result, fmt.Errorf("could not interpret output %s type: %w", output.ID, err) 499 } 500 501 result, err = gocty.ToCtyValue(output.Value, ctype) 502 if err != nil { 503 return result, fmt.Errorf("could not interpret value %v as type %s for output %s: %w", result, ctype.FriendlyName(), output.ID, err) 504 } 505 506 return result, nil 507 }