github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/gcs/client.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package gcs 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "strconv" 11 12 "cloud.google.com/go/storage" 13 multierror "github.com/hashicorp/go-multierror" 14 "github.com/terramate-io/tf/states/remote" 15 "github.com/terramate-io/tf/states/statemgr" 16 "golang.org/x/net/context" 17 ) 18 19 // remoteClient is used by "state/remote".State to read and write 20 // blobs representing state. 21 // Implements "state/remote".ClientLocker 22 type remoteClient struct { 23 storageContext context.Context 24 storageClient *storage.Client 25 bucketName string 26 stateFilePath string 27 lockFilePath string 28 encryptionKey []byte 29 kmsKeyName string 30 } 31 32 func (c *remoteClient) Get() (payload *remote.Payload, err error) { 33 stateFileReader, err := c.stateFile().NewReader(c.storageContext) 34 if err != nil { 35 if err == storage.ErrObjectNotExist { 36 return nil, nil 37 } else { 38 return nil, fmt.Errorf("Failed to open state file at %v: %v", c.stateFileURL(), err) 39 } 40 } 41 defer stateFileReader.Close() 42 43 stateFileContents, err := ioutil.ReadAll(stateFileReader) 44 if err != nil { 45 return nil, fmt.Errorf("Failed to read state file from %v: %v", c.stateFileURL(), err) 46 } 47 48 stateFileAttrs, err := c.stateFile().Attrs(c.storageContext) 49 if err != nil { 50 return nil, fmt.Errorf("Failed to read state file attrs from %v: %v", c.stateFileURL(), err) 51 } 52 53 result := &remote.Payload{ 54 Data: stateFileContents, 55 MD5: stateFileAttrs.MD5, 56 } 57 58 return result, nil 59 } 60 61 func (c *remoteClient) Put(data []byte) error { 62 err := func() error { 63 stateFileWriter := c.stateFile().NewWriter(c.storageContext) 64 if len(c.kmsKeyName) > 0 { 65 stateFileWriter.KMSKeyName = c.kmsKeyName 66 } 67 if _, err := stateFileWriter.Write(data); err != nil { 68 return err 69 } 70 return stateFileWriter.Close() 71 }() 72 if err != nil { 73 return fmt.Errorf("Failed to upload state to %v: %v", c.stateFileURL(), err) 74 } 75 76 return nil 77 } 78 79 func (c *remoteClient) Delete() error { 80 if err := c.stateFile().Delete(c.storageContext); err != nil { 81 return fmt.Errorf("Failed to delete state file %v: %v", c.stateFileURL(), err) 82 } 83 84 return nil 85 } 86 87 // Lock writes to a lock file, ensuring file creation. Returns the generation 88 // number, which must be passed to Unlock(). 89 func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { 90 // update the path we're using 91 // we can't set the ID until the info is written 92 info.Path = c.lockFileURL() 93 94 infoJson, err := json.Marshal(info) 95 if err != nil { 96 return "", err 97 } 98 99 lockFile := c.lockFile() 100 w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(c.storageContext) 101 err = func() error { 102 if _, err := w.Write(infoJson); err != nil { 103 return err 104 } 105 return w.Close() 106 }() 107 108 if err != nil { 109 return "", c.lockError(fmt.Errorf("writing %q failed: %v", c.lockFileURL(), err)) 110 } 111 112 info.ID = strconv.FormatInt(w.Attrs().Generation, 10) 113 114 return info.ID, nil 115 } 116 117 func (c *remoteClient) Unlock(id string) error { 118 gen, err := strconv.ParseInt(id, 10, 64) 119 if err != nil { 120 return fmt.Errorf("Lock ID should be numerical value, got '%s'", id) 121 } 122 123 if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(c.storageContext); err != nil { 124 return c.lockError(err) 125 } 126 127 return nil 128 } 129 130 func (c *remoteClient) lockError(err error) *statemgr.LockError { 131 lockErr := &statemgr.LockError{ 132 Err: err, 133 } 134 135 info, infoErr := c.lockInfo() 136 if infoErr != nil { 137 lockErr.Err = multierror.Append(lockErr.Err, infoErr) 138 } else { 139 lockErr.Info = info 140 } 141 return lockErr 142 } 143 144 // lockInfo reads the lock file, parses its contents and returns the parsed 145 // LockInfo struct. 146 func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) { 147 r, err := c.lockFile().NewReader(c.storageContext) 148 if err != nil { 149 return nil, err 150 } 151 defer r.Close() 152 153 rawData, err := ioutil.ReadAll(r) 154 if err != nil { 155 return nil, err 156 } 157 158 info := &statemgr.LockInfo{} 159 if err := json.Unmarshal(rawData, info); err != nil { 160 return nil, err 161 } 162 163 // We use the Generation as the ID, so overwrite the ID in the json. 164 // This can't be written into the Info, since the generation isn't known 165 // until it's written. 166 attrs, err := c.lockFile().Attrs(c.storageContext) 167 if err != nil { 168 return nil, err 169 } 170 info.ID = strconv.FormatInt(attrs.Generation, 10) 171 172 return info, nil 173 } 174 175 func (c *remoteClient) stateFile() *storage.ObjectHandle { 176 h := c.storageClient.Bucket(c.bucketName).Object(c.stateFilePath) 177 if len(c.encryptionKey) > 0 { 178 return h.Key(c.encryptionKey) 179 } 180 return h 181 } 182 183 func (c *remoteClient) stateFileURL() string { 184 return fmt.Sprintf("gs://%v/%v", c.bucketName, c.stateFilePath) 185 } 186 187 func (c *remoteClient) lockFile() *storage.ObjectHandle { 188 return c.storageClient.Bucket(c.bucketName).Object(c.lockFilePath) 189 } 190 191 func (c *remoteClient) lockFileURL() string { 192 return fmt.Sprintf("gs://%v/%v", c.bucketName, c.lockFilePath) 193 }