github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/azure/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 azure
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/opentofu/opentofu/internal/backend"
    15  	"github.com/opentofu/opentofu/internal/states"
    16  	"github.com/opentofu/opentofu/internal/states/remote"
    17  	"github.com/opentofu/opentofu/internal/states/statemgr"
    18  	"github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs"
    19  	"github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers"
    20  )
    21  
    22  const (
    23  	// This will be used as directory name, the odd looking colon is simply to
    24  	// reduce the chance of name conflicts with existing objects.
    25  	keyEnvPrefix = "env:"
    26  )
    27  
    28  func (b *Backend) Workspaces() ([]string, error) {
    29  	prefix := b.keyName + keyEnvPrefix
    30  	params := containers.ListBlobsInput{
    31  		Prefix: &prefix,
    32  	}
    33  
    34  	ctx := context.TODO()
    35  	client, err := b.armClient.getContainersClient(ctx)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  	resp, err := client.ListBlobs(ctx, b.armClient.storageAccountName, b.containerName, params)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	envs := map[string]struct{}{}
    45  	for _, obj := range resp.Blobs.Blobs {
    46  		key := obj.Name
    47  		if strings.HasPrefix(key, prefix) {
    48  			name := strings.TrimPrefix(key, prefix)
    49  			// we store the state in a key, not a directory
    50  			if strings.Contains(name, "/") {
    51  				continue
    52  			}
    53  
    54  			envs[name] = struct{}{}
    55  		}
    56  	}
    57  
    58  	result := []string{backend.DefaultStateName}
    59  	for name := range envs {
    60  		result = append(result, name)
    61  	}
    62  	sort.Strings(result[1:])
    63  	return result, nil
    64  }
    65  
    66  func (b *Backend) DeleteWorkspace(name string, _ bool) error {
    67  	if name == backend.DefaultStateName || name == "" {
    68  		return fmt.Errorf("can't delete default state")
    69  	}
    70  
    71  	ctx := context.TODO()
    72  	client, err := b.armClient.getBlobClient(ctx)
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	if resp, err := client.Delete(ctx, b.armClient.storageAccountName, b.containerName, b.path(name), blobs.DeleteInput{}); err != nil {
    78  		if resp.Response.StatusCode != 404 {
    79  			return err
    80  		}
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
    87  	ctx := context.TODO()
    88  	blobClient, err := b.armClient.getBlobClient(ctx)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	client := &RemoteClient{
    94  		giovanniBlobClient: *blobClient,
    95  		containerName:      b.containerName,
    96  		keyName:            b.path(name),
    97  		accountName:        b.accountName,
    98  		snapshot:           b.snapshot,
    99  	}
   100  
   101  	stateMgr := remote.NewState(client, b.encryption)
   102  
   103  	// Grab the value
   104  	if err := stateMgr.RefreshState(); err != nil {
   105  		return nil, err
   106  	}
   107  	//if this isn't the default state name, we need to create the object so
   108  	//it's listed by States.
   109  	if v := stateMgr.State(); v == nil {
   110  		// take a lock on this state while we write it
   111  		lockInfo := statemgr.NewLockInfo()
   112  		lockInfo.Operation = "init"
   113  		lockId, err := client.Lock(lockInfo)
   114  		if err != nil {
   115  			return nil, fmt.Errorf("failed to lock azure state: %w", err)
   116  		}
   117  
   118  		// Local helper function so we can call it multiple places
   119  		lockUnlock := func(parent error) error {
   120  			if err := stateMgr.Unlock(lockId); err != nil {
   121  				return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
   122  			}
   123  			return parent
   124  		}
   125  
   126  		// Grab the value
   127  		if err := stateMgr.RefreshState(); err != nil {
   128  			err = lockUnlock(err)
   129  			return nil, err
   130  		}
   131  		//if this isn't the default state name, we need to create the object so
   132  		//it's listed by States.
   133  		if v := stateMgr.State(); v == nil {
   134  			// If we have no state, we have to create an empty state
   135  			if err := stateMgr.WriteState(states.NewState()); err != nil {
   136  				err = lockUnlock(err)
   137  				return nil, err
   138  			}
   139  			if err := stateMgr.PersistState(nil); err != nil {
   140  				err = lockUnlock(err)
   141  				return nil, err
   142  			}
   143  
   144  			// Unlock, the state should now be initialized
   145  			if err := lockUnlock(nil); err != nil {
   146  				return nil, err
   147  			}
   148  		}
   149  	}
   150  
   151  	return stateMgr, nil
   152  }
   153  
   154  func (b *Backend) client() *RemoteClient {
   155  	return &RemoteClient{}
   156  }
   157  
   158  func (b *Backend) path(name string) string {
   159  	if name == backend.DefaultStateName {
   160  		return b.keyName
   161  	}
   162  
   163  	return b.keyName + keyEnvPrefix + name
   164  }
   165  
   166  const errStateUnlock = `
   167  Error unlocking Azure state. Lock ID: %s
   168  
   169  Error: %w
   170  
   171  You may have to force-unlock this state in order to use it again.
   172  `