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  }