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