kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/backend/remote-state/oss/backend_state.go (about)

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