github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/oss/backend_state.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package oss
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"log"
    10  	"path"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/aliyun/aliyun-oss-go-sdk/oss"
    15  	"github.com/aliyun/aliyun-tablestore-go-sdk/tablestore"
    16  
    17  	"github.com/terramate-io/tf/backend"
    18  	"github.com/terramate-io/tf/states"
    19  	"github.com/terramate-io/tf/states/remote"
    20  	"github.com/terramate-io/tf/states/statemgr"
    21  )
    22  
    23  const (
    24  	lockFileSuffix = ".tflock"
    25  )
    26  
    27  // get a remote client configured for this state
    28  func (b *Backend) remoteClient(name string) (*RemoteClient, error) {
    29  	if name == "" {
    30  		return nil, errors.New("missing state name")
    31  	}
    32  
    33  	client := &RemoteClient{
    34  		ossClient:            b.ossClient,
    35  		bucketName:           b.bucketName,
    36  		stateFile:            b.stateFile(name),
    37  		lockFile:             b.lockFile(name),
    38  		serverSideEncryption: b.serverSideEncryption,
    39  		acl:                  b.acl,
    40  		otsTable:             b.otsTable,
    41  		otsClient:            b.otsClient,
    42  	}
    43  	if b.otsEndpoint != "" && b.otsTable != "" {
    44  		_, err := b.otsClient.DescribeTable(&tablestore.DescribeTableRequest{
    45  			TableName: b.otsTable,
    46  		})
    47  		if err != nil {
    48  			return client, fmt.Errorf("error describing table store %s: %#v", b.otsTable, err)
    49  		}
    50  	}
    51  
    52  	return client, nil
    53  }
    54  
    55  func (b *Backend) Workspaces() ([]string, error) {
    56  	bucket, err := b.ossClient.Bucket(b.bucketName)
    57  	if err != nil {
    58  		return []string{""}, fmt.Errorf("error getting bucket: %#v", err)
    59  	}
    60  
    61  	var options []oss.Option
    62  	options = append(options, oss.Prefix(b.statePrefix+"/"), oss.MaxKeys(1000))
    63  	resp, err := bucket.ListObjects(options...)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	result := []string{backend.DefaultStateName}
    69  	prefix := b.statePrefix
    70  	lastObj := ""
    71  	for {
    72  		for _, obj := range resp.Objects {
    73  			// we have 3 parts, the state prefix, the workspace name, and the state file: <prefix>/<worksapce-name>/<key>
    74  			if path.Join(b.statePrefix, b.stateKey) == obj.Key {
    75  				// filter the default workspace
    76  				continue
    77  			}
    78  			lastObj = obj.Key
    79  			parts := strings.Split(strings.TrimPrefix(obj.Key, prefix+"/"), "/")
    80  			if len(parts) > 0 && parts[0] != "" {
    81  				result = append(result, parts[0])
    82  			}
    83  		}
    84  		if resp.IsTruncated {
    85  			if len(options) == 3 {
    86  				options[2] = oss.Marker(lastObj)
    87  			} else {
    88  				options = append(options, oss.Marker(lastObj))
    89  			}
    90  			resp, err = bucket.ListObjects(options...)
    91  			if err != nil {
    92  				return nil, err
    93  			}
    94  		} else {
    95  			break
    96  		}
    97  	}
    98  	sort.Strings(result[1:])
    99  	return result, nil
   100  }
   101  
   102  func (b *Backend) DeleteWorkspace(name string, _ bool) error {
   103  	if name == backend.DefaultStateName || name == "" {
   104  		return fmt.Errorf("can't delete default state")
   105  	}
   106  
   107  	client, err := b.remoteClient(name)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	return client.Delete()
   112  }
   113  
   114  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
   115  	client, err := b.remoteClient(name)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	stateMgr := &remote.State{Client: client}
   120  
   121  	// Check to see if this state already exists.
   122  	existing, err := b.Workspaces()
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	log.Printf("[DEBUG] Current workspace name: %s. All workspaces:%#v", name, existing)
   128  
   129  	exists := false
   130  	for _, s := range existing {
   131  		if s == name {
   132  			exists = true
   133  			break
   134  		}
   135  	}
   136  	// We need to create the object so it's listed by States.
   137  	if !exists {
   138  		// take a lock on this state while we write it
   139  		lockInfo := statemgr.NewLockInfo()
   140  		lockInfo.Operation = "init"
   141  		lockId, err := client.Lock(lockInfo)
   142  		if err != nil {
   143  			return nil, fmt.Errorf("failed to lock OSS state: %s", err)
   144  		}
   145  
   146  		// Local helper function so we can call it multiple places
   147  		lockUnlock := func(e error) error {
   148  			if err := stateMgr.Unlock(lockId); err != nil {
   149  				return fmt.Errorf(strings.TrimSpace(stateUnlockError), lockId, err)
   150  			}
   151  			return e
   152  		}
   153  
   154  		// Grab the value
   155  		if err := stateMgr.RefreshState(); err != nil {
   156  			err = lockUnlock(err)
   157  			return nil, err
   158  		}
   159  
   160  		// If we have no state, we have to create an empty state
   161  		if v := stateMgr.State(); v == nil {
   162  			if err := stateMgr.WriteState(states.NewState()); err != nil {
   163  				err = lockUnlock(err)
   164  				return nil, err
   165  			}
   166  			if err := stateMgr.PersistState(nil); err != nil {
   167  				err = lockUnlock(err)
   168  				return nil, err
   169  			}
   170  		}
   171  
   172  		// Unlock, the state should now be initialized
   173  		if err := lockUnlock(nil); err != nil {
   174  			return nil, err
   175  		}
   176  
   177  	}
   178  	return stateMgr, nil
   179  }
   180  
   181  func (b *Backend) stateFile(name string) string {
   182  	if name == backend.DefaultStateName {
   183  		return path.Join(b.statePrefix, b.stateKey)
   184  	}
   185  	return path.Join(b.statePrefix, name, b.stateKey)
   186  }
   187  
   188  func (b *Backend) lockFile(name string) string {
   189  	return b.stateFile(name) + lockFileSuffix
   190  }
   191  
   192  const stateUnlockError = `
   193  Error unlocking Alibaba Cloud OSS state file:
   194  
   195  Lock ID: %s
   196  Error message: %#v
   197  
   198  You may have to force-unlock this state in order to use it again.
   199  The Alibaba Cloud backend acquires a lock during initialization to ensure the initial state file is created.
   200  `