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 `