github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/s3/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 s3
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"path"
    13  	"sort"
    14  	"strings"
    15  
    16  	"github.com/aws/aws-sdk-go-v2/aws"
    17  	"github.com/aws/aws-sdk-go-v2/service/s3"
    18  	types "github.com/aws/aws-sdk-go-v2/service/s3/types"
    19  	"github.com/aws/smithy-go"
    20  
    21  	"github.com/opentofu/opentofu/internal/backend"
    22  	"github.com/opentofu/opentofu/internal/states"
    23  	"github.com/opentofu/opentofu/internal/states/remote"
    24  	"github.com/opentofu/opentofu/internal/states/statemgr"
    25  )
    26  
    27  func (b *Backend) Workspaces() ([]string, error) {
    28  	const maxKeys = 1000
    29  
    30  	prefix := ""
    31  
    32  	if b.workspaceKeyPrefix != "" {
    33  		prefix = b.workspaceKeyPrefix + "/"
    34  	}
    35  
    36  	params := &s3.ListObjectsV2Input{
    37  		Bucket:  aws.String(b.bucketName),
    38  		Prefix:  aws.String(prefix),
    39  		MaxKeys: aws.Int32(maxKeys),
    40  	}
    41  
    42  	ctx := context.TODO()
    43  
    44  	ctx, _ = attachLoggerToContext(ctx)
    45  
    46  	wss := []string{backend.DefaultStateName}
    47  	pg := s3.NewListObjectsV2Paginator(b.s3Client, params)
    48  
    49  	for pg.HasMorePages() {
    50  		page, err := pg.NextPage(ctx)
    51  		if err != nil {
    52  			var noBucketErr *types.NoSuchBucket
    53  			if errors.As(err, &noBucketErr) {
    54  				return nil, fmt.Errorf(errS3NoSuchBucket, err)
    55  			}
    56  
    57  			// Ignoring AccessDenied errors for backward compatibility,
    58  			// since it should work for default state when no other workspaces present.
    59  			var apiErr smithy.APIError
    60  			if errors.As(err, &apiErr) && apiErr.ErrorCode() == "AccessDenied" {
    61  				break
    62  			}
    63  
    64  			return nil, err
    65  		}
    66  
    67  		for _, obj := range page.Contents {
    68  			ws := b.keyEnv(*obj.Key)
    69  			if ws != "" {
    70  				wss = append(wss, ws)
    71  			}
    72  		}
    73  	}
    74  
    75  	sort.Strings(wss[1:])
    76  	return wss, nil
    77  }
    78  
    79  func (b *Backend) keyEnv(key string) string {
    80  	prefix := b.workspaceKeyPrefix
    81  
    82  	if prefix == "" {
    83  		parts := strings.SplitN(key, "/", 2)
    84  		if len(parts) > 1 && parts[1] == b.keyName {
    85  			return parts[0]
    86  		} else {
    87  			return ""
    88  		}
    89  	}
    90  
    91  	// add a slash to treat this as a directory
    92  	prefix += "/"
    93  
    94  	parts := strings.SplitAfterN(key, prefix, 2)
    95  	if len(parts) < 2 {
    96  		return ""
    97  	}
    98  
    99  	// shouldn't happen since we listed by prefix
   100  	if parts[0] != prefix {
   101  		return ""
   102  	}
   103  
   104  	parts = strings.SplitN(parts[1], "/", 2)
   105  
   106  	if len(parts) < 2 {
   107  		return ""
   108  	}
   109  
   110  	// not our key, so don't include it in our listing
   111  	if parts[1] != b.keyName {
   112  		return ""
   113  	}
   114  
   115  	return parts[0]
   116  }
   117  
   118  func (b *Backend) DeleteWorkspace(name string, _ bool) error {
   119  	if name == backend.DefaultStateName || name == "" {
   120  		return fmt.Errorf("can't delete default state")
   121  	}
   122  
   123  	client, err := b.remoteClient(name)
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	return client.Delete()
   129  }
   130  
   131  // get a remote client configured for this state
   132  func (b *Backend) remoteClient(name string) (*RemoteClient, error) {
   133  	if name == "" {
   134  		return nil, errors.New("missing state name")
   135  	}
   136  
   137  	client := &RemoteClient{
   138  		s3Client:              b.s3Client,
   139  		dynClient:             b.dynClient,
   140  		bucketName:            b.bucketName,
   141  		path:                  b.path(name),
   142  		serverSideEncryption:  b.serverSideEncryption,
   143  		customerEncryptionKey: b.customerEncryptionKey,
   144  		acl:                   b.acl,
   145  		kmsKeyID:              b.kmsKeyID,
   146  		ddbTable:              b.ddbTable,
   147  		skipS3Checksum:        b.skipS3Checksum,
   148  	}
   149  
   150  	return client, nil
   151  }
   152  
   153  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
   154  	client, err := b.remoteClient(name)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	stateMgr := remote.NewState(client, b.encryption)
   160  	// Check to see if this state already exists.
   161  	// If we're trying to force-unlock a state, we can't take the lock before
   162  	// fetching the state. If the state doesn't exist, we have to assume this
   163  	// is a normal create operation, and take the lock at that point.
   164  	//
   165  	// If we need to force-unlock, but for some reason the state no longer
   166  	// exists, the user will have to use aws tools to manually fix the
   167  	// situation.
   168  	existing, err := b.Workspaces()
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	exists := false
   174  	for _, s := range existing {
   175  		if s == name {
   176  			exists = true
   177  			break
   178  		}
   179  	}
   180  
   181  	// We need to create the object so it's listed by States.
   182  	if !exists {
   183  		// take a lock on this state while we write it
   184  		lockInfo := statemgr.NewLockInfo()
   185  		lockInfo.Operation = "init"
   186  		lockId, err := client.Lock(lockInfo)
   187  		if err != nil {
   188  			return nil, fmt.Errorf("failed to lock s3 state: %w", err)
   189  		}
   190  
   191  		// Local helper function so we can call it multiple places
   192  		lockUnlock := func(parent error) error {
   193  			if err := stateMgr.Unlock(lockId); err != nil {
   194  				return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
   195  			}
   196  			return parent
   197  		}
   198  
   199  		// Grab the value
   200  		// This is to ensure that no one beat us to writing a state between
   201  		// the `exists` check and taking the lock.
   202  		if err := stateMgr.RefreshState(); err != nil {
   203  			err = lockUnlock(err)
   204  			return nil, err
   205  		}
   206  
   207  		// If we have no state, we have to create an empty state
   208  		if v := stateMgr.State(); v == nil {
   209  			if err := stateMgr.WriteState(states.NewState()); err != nil {
   210  				err = lockUnlock(err)
   211  				return nil, err
   212  			}
   213  			if err := stateMgr.PersistState(nil); err != nil {
   214  				err = lockUnlock(err)
   215  				return nil, err
   216  			}
   217  		}
   218  
   219  		// Unlock, the state should now be initialized
   220  		if err := lockUnlock(nil); err != nil {
   221  			return nil, err
   222  		}
   223  
   224  	}
   225  
   226  	return stateMgr, nil
   227  }
   228  
   229  func (b *Backend) path(name string) string {
   230  	if name == backend.DefaultStateName {
   231  		return b.keyName
   232  	}
   233  
   234  	return path.Join(b.workspaceKeyPrefix, name, b.keyName)
   235  }
   236  
   237  const errStateUnlock = `
   238  Error unlocking S3 state. Lock ID: %s
   239  
   240  Error: %s
   241  
   242  You may have to force-unlock this state in order to use it again.
   243  `