github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/remote-state/oss/client.go (about)

     1  package oss
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"log"
    11  	"time"
    12  
    13  	"github.com/aliyun/aliyun-oss-go-sdk/oss"
    14  	"github.com/aliyun/aliyun-tablestore-go-sdk/tablestore"
    15  	"github.com/hashicorp/go-multierror"
    16  	uuid "github.com/hashicorp/go-uuid"
    17  	"github.com/pkg/errors"
    18  
    19  	"github.com/hashicorp/terraform/internal/states/remote"
    20  	"github.com/hashicorp/terraform/internal/states/statemgr"
    21  )
    22  
    23  const (
    24  	// Store the last saved serial in tablestore with this suffix for consistency checks.
    25  	stateIDSuffix = "-md5"
    26  
    27  	pkName = "LockID"
    28  )
    29  
    30  var (
    31  	// The amount of time we will retry a state waiting for it to match the
    32  	// expected checksum.
    33  	consistencyRetryTimeout = 10 * time.Second
    34  
    35  	// delay when polling the state
    36  	consistencyRetryPollInterval = 2 * time.Second
    37  )
    38  
    39  // test hook called when checksums don't match
    40  var testChecksumHook func()
    41  
    42  type RemoteClient struct {
    43  	ossClient            *oss.Client
    44  	otsClient            *tablestore.TableStoreClient
    45  	bucketName           string
    46  	stateFile            string
    47  	lockFile             string
    48  	serverSideEncryption bool
    49  	acl                  string
    50  	otsTable             string
    51  }
    52  
    53  func (c *RemoteClient) Get() (payload *remote.Payload, err error) {
    54  	deadline := time.Now().Add(consistencyRetryTimeout)
    55  
    56  	// If we have a checksum, and the returned payload doesn't match, we retry
    57  	// up until deadline.
    58  	for {
    59  		payload, err = c.getObj()
    60  		if err != nil {
    61  			return nil, err
    62  		}
    63  
    64  		// If the remote state was manually removed the payload will be nil,
    65  		// but if there's still a digest entry for that state we will still try
    66  		// to compare the MD5 below.
    67  		var digest []byte
    68  		if payload != nil {
    69  			digest = payload.MD5
    70  		}
    71  
    72  		// verify that this state is what we expect
    73  		if expected, err := c.getMD5(); err != nil {
    74  			log.Printf("[WARN] failed to fetch state md5: %s", err)
    75  		} else if len(expected) > 0 && !bytes.Equal(expected, digest) {
    76  			log.Printf("[WARN] state md5 mismatch: expected '%x', got '%x'", expected, digest)
    77  
    78  			if testChecksumHook != nil {
    79  				testChecksumHook()
    80  			}
    81  
    82  			if time.Now().Before(deadline) {
    83  				time.Sleep(consistencyRetryPollInterval)
    84  				log.Println("[INFO] retrying OSS RemoteClient.Get...")
    85  				continue
    86  			}
    87  
    88  			return nil, fmt.Errorf(errBadChecksumFmt, digest)
    89  		}
    90  
    91  		break
    92  	}
    93  	return payload, nil
    94  }
    95  
    96  func (c *RemoteClient) Put(data []byte) error {
    97  	bucket, err := c.ossClient.Bucket(c.bucketName)
    98  	if err != nil {
    99  		return fmt.Errorf("error getting bucket: %#v", err)
   100  	}
   101  
   102  	body := bytes.NewReader(data)
   103  
   104  	var options []oss.Option
   105  	if c.acl != "" {
   106  		options = append(options, oss.ACL(oss.ACLType(c.acl)))
   107  	}
   108  	options = append(options, oss.ContentType("application/json"))
   109  	if c.serverSideEncryption {
   110  		options = append(options, oss.ServerSideEncryption("AES256"))
   111  	}
   112  	options = append(options, oss.ContentLength(int64(len(data))))
   113  
   114  	if body != nil {
   115  		if err := bucket.PutObject(c.stateFile, body, options...); err != nil {
   116  			return fmt.Errorf("failed to upload state %s: %#v", c.stateFile, err)
   117  		}
   118  	}
   119  
   120  	sum := md5.Sum(data)
   121  	if err := c.putMD5(sum[:]); err != nil {
   122  		// if this errors out, we unfortunately have to error out altogether,
   123  		// since the next Get will inevitably fail.
   124  		return fmt.Errorf("failed to store state MD5: %s", err)
   125  	}
   126  	return nil
   127  }
   128  
   129  func (c *RemoteClient) Delete() error {
   130  	bucket, err := c.ossClient.Bucket(c.bucketName)
   131  	if err != nil {
   132  		return fmt.Errorf("error getting bucket %s: %#v", c.bucketName, err)
   133  	}
   134  
   135  	log.Printf("[DEBUG] Deleting remote state from OSS: %#v", c.stateFile)
   136  
   137  	if err := bucket.DeleteObject(c.stateFile); err != nil {
   138  		return fmt.Errorf("error deleting state %s: %#v", c.stateFile, err)
   139  	}
   140  
   141  	if err := c.deleteMD5(); err != nil {
   142  		log.Printf("[WARN] Error deleting state MD5: %s", err)
   143  	}
   144  	return nil
   145  }
   146  
   147  func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
   148  	if c.otsTable == "" {
   149  		return "", nil
   150  	}
   151  
   152  	info.Path = c.lockPath()
   153  
   154  	if info.ID == "" {
   155  		lockID, err := uuid.GenerateUUID()
   156  		if err != nil {
   157  			return "", err
   158  		}
   159  		info.ID = lockID
   160  	}
   161  
   162  	putParams := &tablestore.PutRowChange{
   163  		TableName: c.otsTable,
   164  		PrimaryKey: &tablestore.PrimaryKey{
   165  			PrimaryKeys: []*tablestore.PrimaryKeyColumn{
   166  				{
   167  					ColumnName: pkName,
   168  					Value:      c.lockPath(),
   169  				},
   170  			},
   171  		},
   172  		Columns: []tablestore.AttributeColumn{
   173  			{
   174  				ColumnName: "Info",
   175  				Value:      string(info.Marshal()),
   176  			},
   177  		},
   178  		Condition: &tablestore.RowCondition{
   179  			RowExistenceExpectation: tablestore.RowExistenceExpectation_EXPECT_NOT_EXIST,
   180  		},
   181  	}
   182  
   183  	log.Printf("[DEBUG] Recording state lock in tablestore: %#v; LOCKID:%s", putParams, c.lockPath())
   184  
   185  	_, err := c.otsClient.PutRow(&tablestore.PutRowRequest{
   186  		PutRowChange: putParams,
   187  	})
   188  	if err != nil {
   189  		err = fmt.Errorf("invoking PutRow got an error: %#v", err)
   190  		lockInfo, infoErr := c.getLockInfo()
   191  		if infoErr != nil {
   192  			err = multierror.Append(err, fmt.Errorf("\ngetting lock info got an error: %#v", infoErr))
   193  		}
   194  		lockErr := &statemgr.LockError{
   195  			Err:  err,
   196  			Info: lockInfo,
   197  		}
   198  		log.Printf("[ERROR] state lock error: %s", lockErr.Error())
   199  		return "", lockErr
   200  	}
   201  
   202  	return info.ID, nil
   203  }
   204  
   205  func (c *RemoteClient) getMD5() ([]byte, error) {
   206  	if c.otsTable == "" {
   207  		return nil, nil
   208  	}
   209  
   210  	getParams := &tablestore.SingleRowQueryCriteria{
   211  		TableName: c.otsTable,
   212  		PrimaryKey: &tablestore.PrimaryKey{
   213  			PrimaryKeys: []*tablestore.PrimaryKeyColumn{
   214  				{
   215  					ColumnName: pkName,
   216  					Value:      c.lockPath() + stateIDSuffix,
   217  				},
   218  			},
   219  		},
   220  		ColumnsToGet: []string{pkName, "Digest"},
   221  		MaxVersion:   1,
   222  	}
   223  
   224  	log.Printf("[DEBUG] Retrieving state serial in tablestore: %#v", getParams)
   225  
   226  	object, err := c.otsClient.GetRow(&tablestore.GetRowRequest{
   227  		SingleRowQueryCriteria: getParams,
   228  	})
   229  
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	var val string
   235  	if v, ok := object.GetColumnMap().Columns["Digest"]; ok && len(v) > 0 {
   236  		val = v[0].Value.(string)
   237  	}
   238  
   239  	sum, err := hex.DecodeString(val)
   240  	if err != nil || len(sum) != md5.Size {
   241  		return nil, errors.New("invalid md5")
   242  	}
   243  
   244  	return sum, nil
   245  }
   246  
   247  // store the hash of the state to that clients can check for stale state files.
   248  func (c *RemoteClient) putMD5(sum []byte) error {
   249  	if c.otsTable == "" {
   250  		return nil
   251  	}
   252  
   253  	if len(sum) != md5.Size {
   254  		return errors.New("invalid payload md5")
   255  	}
   256  
   257  	putParams := &tablestore.PutRowChange{
   258  		TableName: c.otsTable,
   259  		PrimaryKey: &tablestore.PrimaryKey{
   260  			PrimaryKeys: []*tablestore.PrimaryKeyColumn{
   261  				{
   262  					ColumnName: pkName,
   263  					Value:      c.lockPath() + stateIDSuffix,
   264  				},
   265  			},
   266  		},
   267  		Columns: []tablestore.AttributeColumn{
   268  			{
   269  				ColumnName: "Digest",
   270  				Value:      hex.EncodeToString(sum),
   271  			},
   272  		},
   273  		Condition: &tablestore.RowCondition{
   274  			RowExistenceExpectation: tablestore.RowExistenceExpectation_IGNORE,
   275  		},
   276  	}
   277  
   278  	log.Printf("[DEBUG] Recoring state serial in tablestore: %#v", putParams)
   279  
   280  	_, err := c.otsClient.PutRow(&tablestore.PutRowRequest{
   281  		PutRowChange: putParams,
   282  	})
   283  
   284  	if err != nil {
   285  		log.Printf("[WARN] failed to record state serial in tablestore: %s", err)
   286  	}
   287  
   288  	return nil
   289  }
   290  
   291  // remove the hash value for a deleted state
   292  func (c *RemoteClient) deleteMD5() error {
   293  	if c.otsTable == "" {
   294  		return nil
   295  	}
   296  
   297  	params := &tablestore.DeleteRowRequest{
   298  		DeleteRowChange: &tablestore.DeleteRowChange{
   299  			TableName: c.otsTable,
   300  			PrimaryKey: &tablestore.PrimaryKey{
   301  				PrimaryKeys: []*tablestore.PrimaryKeyColumn{
   302  					{
   303  						ColumnName: pkName,
   304  						Value:      c.lockPath() + stateIDSuffix,
   305  					},
   306  				},
   307  			},
   308  			Condition: &tablestore.RowCondition{
   309  				RowExistenceExpectation: tablestore.RowExistenceExpectation_EXPECT_EXIST,
   310  			},
   311  		},
   312  	}
   313  
   314  	log.Printf("[DEBUG] Deleting state serial in tablestore: %#v", params)
   315  
   316  	if _, err := c.otsClient.DeleteRow(params); err != nil {
   317  		return err
   318  	}
   319  
   320  	return nil
   321  }
   322  
   323  func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {
   324  	getParams := &tablestore.SingleRowQueryCriteria{
   325  		TableName: c.otsTable,
   326  		PrimaryKey: &tablestore.PrimaryKey{
   327  			PrimaryKeys: []*tablestore.PrimaryKeyColumn{
   328  				{
   329  					ColumnName: pkName,
   330  					Value:      c.lockPath(),
   331  				},
   332  			},
   333  		},
   334  		ColumnsToGet: []string{pkName, "Info"},
   335  		MaxVersion:   1,
   336  	}
   337  
   338  	log.Printf("[DEBUG] Retrieving state lock info from tablestore: %#v", getParams)
   339  
   340  	object, err := c.otsClient.GetRow(&tablestore.GetRowRequest{
   341  		SingleRowQueryCriteria: getParams,
   342  	})
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  
   347  	var infoData string
   348  	if v, ok := object.GetColumnMap().Columns["Info"]; ok && len(v) > 0 {
   349  		infoData = v[0].Value.(string)
   350  	}
   351  	lockInfo := &statemgr.LockInfo{}
   352  	err = json.Unmarshal([]byte(infoData), lockInfo)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	return lockInfo, nil
   357  }
   358  func (c *RemoteClient) Unlock(id string) error {
   359  	if c.otsTable == "" {
   360  		return nil
   361  	}
   362  
   363  	lockErr := &statemgr.LockError{}
   364  
   365  	lockInfo, err := c.getLockInfo()
   366  	if err != nil {
   367  		lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
   368  		return lockErr
   369  	}
   370  	lockErr.Info = lockInfo
   371  
   372  	if lockInfo.ID != id {
   373  		lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
   374  		return lockErr
   375  	}
   376  	params := &tablestore.DeleteRowRequest{
   377  		DeleteRowChange: &tablestore.DeleteRowChange{
   378  			TableName: c.otsTable,
   379  			PrimaryKey: &tablestore.PrimaryKey{
   380  				PrimaryKeys: []*tablestore.PrimaryKeyColumn{
   381  					{
   382  						ColumnName: pkName,
   383  						Value:      c.lockPath(),
   384  					},
   385  				},
   386  			},
   387  			Condition: &tablestore.RowCondition{
   388  				RowExistenceExpectation: tablestore.RowExistenceExpectation_IGNORE,
   389  			},
   390  		},
   391  	}
   392  
   393  	_, err = c.otsClient.DeleteRow(params)
   394  
   395  	if err != nil {
   396  		lockErr.Err = err
   397  		return lockErr
   398  	}
   399  
   400  	return nil
   401  }
   402  
   403  func (c *RemoteClient) lockPath() string {
   404  	return fmt.Sprintf("%s/%s", c.bucketName, c.stateFile)
   405  }
   406  
   407  func (c *RemoteClient) getObj() (*remote.Payload, error) {
   408  	bucket, err := c.ossClient.Bucket(c.bucketName)
   409  	if err != nil {
   410  		return nil, fmt.Errorf("error getting bucket %s: %#v", c.bucketName, err)
   411  	}
   412  
   413  	if exist, err := bucket.IsObjectExist(c.stateFile); err != nil {
   414  		return nil, fmt.Errorf("estimating object %s is exist got an error: %#v", c.stateFile, err)
   415  	} else if !exist {
   416  		return nil, nil
   417  	}
   418  
   419  	var options []oss.Option
   420  	output, err := bucket.GetObject(c.stateFile, options...)
   421  	if err != nil {
   422  		return nil, fmt.Errorf("error getting object: %#v", err)
   423  	}
   424  
   425  	buf := bytes.NewBuffer(nil)
   426  	if _, err := io.Copy(buf, output); err != nil {
   427  		return nil, fmt.Errorf("failed to read remote state: %s", err)
   428  	}
   429  	sum := md5.Sum(buf.Bytes())
   430  	payload := &remote.Payload{
   431  		Data: buf.Bytes(),
   432  		MD5:  sum[:],
   433  	}
   434  
   435  	// If there was no data, then return nil
   436  	if len(payload.Data) == 0 {
   437  		return nil, nil
   438  	}
   439  
   440  	return payload, nil
   441  }
   442  
   443  const errBadChecksumFmt = `state data in OSS does not have the expected content.
   444  
   445  This may be caused by unusually long delays in OSS processing a previous state
   446  update.  Please wait for a minute or two and try again. If this problem
   447  persists, and neither OSS nor TableStore are experiencing an outage, you may need
   448  to manually verify the remote state and update the Digest value stored in the
   449  TableStore table to the following value: %x`