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  }