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

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