github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/oss/backend_state.go (about)

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