github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote-state/swift/client.go (about) 1 package swift 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/md5" 7 "encoding/json" 8 "fmt" 9 "log" 10 "sync" 11 "time" 12 13 "github.com/gophercloud/gophercloud" 14 "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers" 15 "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects" 16 "github.com/gophercloud/gophercloud/pagination" 17 "github.com/hashicorp/terraform/state" 18 "github.com/hashicorp/terraform/state/remote" 19 ) 20 21 const ( 22 consistencyTimeout = 15 23 24 // Suffix that will be appended to state file paths 25 // when locking 26 lockSuffix = ".lock" 27 28 // The TTL associated with this lock. 29 lockTTL = 60 * time.Second 30 31 // The Interval associated with this lock periodic renew. 32 lockRenewInterval = 30 * time.Second 33 34 // The amount of time we will retry to delete a container waiting for 35 // the objects to be deleted. 36 deleteRetryTimeout = 60 * time.Second 37 38 // delay when polling the objects 39 deleteRetryPollInterval = 5 * time.Second 40 ) 41 42 // RemoteClient implements the Client interface for an Openstack Swift server. 43 // Implements "state/remote".ClientLocker 44 type RemoteClient struct { 45 client *gophercloud.ServiceClient 46 container string 47 archive bool 48 archiveContainer string 49 expireSecs int 50 objectName string 51 52 mu sync.Mutex 53 // lockState is true if we're using locks 54 lockState bool 55 56 info *state.LockInfo 57 58 // lockCancel cancels the Context use for lockRenewPeriodic, and is 59 // called when unlocking, or before creating a new lock if the lock is 60 // lost. 61 lockCancel context.CancelFunc 62 } 63 64 func (c *RemoteClient) ListObjectsNames(prefix string, delim string) ([]string, error) { 65 if err := c.ensureContainerExists(); err != nil { 66 return nil, err 67 } 68 69 // List our raw path 70 listOpts := objects.ListOpts{ 71 Full: false, 72 Prefix: prefix, 73 Delimiter: delim, 74 } 75 76 result := []string{} 77 pager := objects.List(c.client, c.container, listOpts) 78 // Define an anonymous function to be executed on each page's iteration 79 err := pager.EachPage(func(page pagination.Page) (bool, error) { 80 objectList, err := objects.ExtractNames(page) 81 if err != nil { 82 return false, fmt.Errorf("Error extracting names from objects from page %+v", err) 83 } 84 for _, object := range objectList { 85 result = append(result, object) 86 } 87 return true, nil 88 }) 89 90 if err != nil { 91 return nil, err 92 } 93 94 return result, nil 95 96 } 97 98 func (c *RemoteClient) Get() (*remote.Payload, error) { 99 payload, err := c.get(c.objectName) 100 101 // 404 response is to be expected if the object doesn't already exist! 102 if _, ok := err.(gophercloud.ErrDefault404); ok { 103 log.Println("[DEBUG] Object doesn't exist to download.") 104 return nil, nil 105 } 106 107 return payload, err 108 } 109 110 // swift is eventually constistent. Consistency 111 // is ensured by the Get func which will always try 112 // to retrieve the most recent object 113 func (c *RemoteClient) Put(data []byte) error { 114 if c.expireSecs != 0 { 115 log.Printf("[DEBUG] ExpireSecs = %d", c.expireSecs) 116 return c.put(c.objectName, data, c.expireSecs, "") 117 } 118 119 return c.put(c.objectName, data, -1, "") 120 121 } 122 123 func (c *RemoteClient) Delete() error { 124 return c.delete(c.objectName) 125 } 126 127 func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) { 128 c.mu.Lock() 129 defer c.mu.Unlock() 130 131 if !c.lockState { 132 return "", nil 133 } 134 135 log.Printf("[DEBUG] Acquiring Lock %#v on %s/%s", info, c.container, c.objectName) 136 137 // This check only is to ensure we strictly follow the specification. 138 // Terraform shouldn't ever re-lock, so provide errors for the possible 139 // states if this is called. 140 if c.info != nil { 141 // we have an active lock already 142 return "", fmt.Errorf("state %q already locked", c.lockFilePath()) 143 } 144 145 // update the path we're using 146 info.Path = c.lockFilePath() 147 148 if err := c.writeLockInfo(info, lockTTL, "*"); err != nil { 149 return "", err 150 } 151 152 log.Printf("[DEBUG] Acquired Lock %s on %s", info.ID, c.objectName) 153 154 c.info = info 155 156 ctx, cancel := context.WithCancel(context.Background()) 157 c.lockCancel = cancel 158 159 // keep the lock renewed 160 go c.lockRenewPeriodic(ctx, info) 161 162 return info.ID, nil 163 } 164 165 func (c *RemoteClient) Unlock(id string) error { 166 c.mu.Lock() 167 168 if !c.lockState { 169 return nil 170 } 171 172 defer func() { 173 // The periodic lock renew is canceled 174 // the lockCancel func may not be nil in most usecases 175 // but can typically be nil when using a second client 176 // to ForceUnlock the state based on the same lock Id 177 if c.lockCancel != nil { 178 c.lockCancel() 179 } 180 c.info = nil 181 c.mu.Unlock() 182 }() 183 184 log.Printf("[DEBUG] Releasing Lock %s on %s", id, c.objectName) 185 186 info, err := c.lockInfo() 187 if err != nil { 188 return c.lockError(fmt.Errorf("failed to retrieve lock info: %s", err), nil) 189 } 190 191 c.info = info 192 193 // conflicting lock 194 if info.ID != id { 195 return c.lockError(fmt.Errorf("lock id %q does not match existing lock", id), info) 196 } 197 198 // before the lock object deletion is ordered, we shall 199 // stop periodic renew 200 if c.lockCancel != nil { 201 c.lockCancel() 202 } 203 204 if err = c.delete(c.lockFilePath()); err != nil { 205 return c.lockError(fmt.Errorf("error deleting lock with %q: %s", id, err), info) 206 } 207 208 // Swift is eventually consistent; we have to wait until 209 // the lock is effectively deleted to return, or raise 210 // an error if deadline is reached. 211 212 warning := ` 213 WARNING: Waiting for lock deletion timed out. 214 Swift has accepted the deletion order of the lock %s/%s. 215 But as it is eventually consistent, complete deletion 216 may happen later. 217 ` 218 deadline := time.Now().Add(deleteRetryTimeout) 219 for { 220 if time.Now().Before(deadline) { 221 info, err := c.lockInfo() 222 223 // 404 response is to be expected if the lock deletion 224 // has been processed 225 if _, ok := err.(gophercloud.ErrDefault404); ok { 226 log.Println("[DEBUG] Lock has been deleted.") 227 return nil 228 } 229 230 if err != nil { 231 return err 232 } 233 234 // conflicting lock 235 if info.ID != id { 236 log.Printf("[DEBUG] Someone else has acquired a lock: %v.", info) 237 return nil 238 } 239 240 log.Printf("[DEBUG] Lock is still there, delete again and wait %v.", deleteRetryPollInterval) 241 c.delete(c.lockFilePath()) 242 time.Sleep(deleteRetryPollInterval) 243 continue 244 } 245 246 return fmt.Errorf(warning, c.container, c.lockFilePath()) 247 } 248 249 } 250 251 func (c *RemoteClient) get(object string) (*remote.Payload, error) { 252 log.Printf("[DEBUG] Getting object %s/%s", c.container, object) 253 result := objects.Download(c.client, c.container, object, objects.DownloadOpts{Newest: true}) 254 255 // Extract any errors from result 256 _, err := result.Extract() 257 if err != nil { 258 return nil, err 259 } 260 261 bytes, err := result.ExtractContent() 262 if err != nil { 263 return nil, err 264 } 265 266 hash := md5.Sum(bytes) 267 payload := &remote.Payload{ 268 Data: bytes, 269 MD5: hash[:md5.Size], 270 } 271 272 return payload, nil 273 } 274 275 func (c *RemoteClient) put(object string, data []byte, deleteAfter int, ifNoneMatch string) error { 276 log.Printf("[DEBUG] Writing object in %s/%s", c.container, object) 277 if err := c.ensureContainerExists(); err != nil { 278 return err 279 } 280 281 contentType := "application/json" 282 contentLength := int64(len(data)) 283 284 createOpts := objects.CreateOpts{ 285 Content: bytes.NewReader(data), 286 ContentType: contentType, 287 ContentLength: int64(contentLength), 288 } 289 290 if deleteAfter >= 0 { 291 createOpts.DeleteAfter = deleteAfter 292 } 293 294 if ifNoneMatch != "" { 295 createOpts.IfNoneMatch = ifNoneMatch 296 } 297 298 result := objects.Create(c.client, c.container, object, createOpts) 299 if result.Err != nil { 300 return result.Err 301 } 302 303 return nil 304 } 305 306 func (c *RemoteClient) deleteContainer() error { 307 log.Printf("[DEBUG] Deleting container %s", c.container) 308 309 warning := ` 310 WARNING: Waiting for container %s deletion timed out. 311 It may have been left in your Openstack account and may incur storage charges. 312 error was: %s 313 ` 314 315 deadline := time.Now().Add(deleteRetryTimeout) 316 317 // Swift is eventually consistent; we have to retry until 318 // all objects are effectively deleted to delete the container 319 // If we still have objects in the container, or raise 320 // an error if deadline is reached 321 for { 322 if time.Now().Before(deadline) { 323 // Remove any objects 324 c.cleanObjects() 325 326 // Delete the container 327 log.Printf("[DEBUG] Deleting container %s", c.container) 328 deleteResult := containers.Delete(c.client, c.container) 329 if deleteResult.Err != nil { 330 // container is not found, thus has been deleted 331 if _, ok := deleteResult.Err.(gophercloud.ErrDefault404); ok { 332 return nil 333 } 334 335 // 409 http error is raised when deleting a container with 336 // remaining objects 337 if respErr, ok := deleteResult.Err.(gophercloud.ErrUnexpectedResponseCode); ok && respErr.Actual == 409 { 338 time.Sleep(deleteRetryPollInterval) 339 log.Printf("[DEBUG] Remaining objects, failed to delete container, retrying...") 340 continue 341 } 342 343 return fmt.Errorf(warning, deleteResult.Err) 344 } 345 return nil 346 } 347 348 return fmt.Errorf(warning, c.container, "timeout reached") 349 } 350 351 } 352 353 // Helper function to delete Swift objects within a container 354 func (c *RemoteClient) cleanObjects() error { 355 // Get a slice of object names 356 objectNames, err := c.objectNames(c.container) 357 if err != nil { 358 return err 359 } 360 361 for _, object := range objectNames { 362 log.Printf("[DEBUG] Deleting object %s from container %s", object, c.container) 363 result := objects.Delete(c.client, c.container, object, nil) 364 if result.Err == nil { 365 continue 366 } 367 368 // if object is not found, it has already been deleted 369 if _, ok := result.Err.(gophercloud.ErrDefault404); !ok { 370 return fmt.Errorf("Error deleting object %s from container %s: %v", object, c.container, result.Err) 371 } 372 } 373 return nil 374 375 } 376 377 func (c *RemoteClient) delete(object string) error { 378 log.Printf("[DEBUG] Deleting object %s/%s", c.container, object) 379 380 result := objects.Delete(c.client, c.container, object, nil) 381 382 if result.Err != nil { 383 return result.Err 384 } 385 return nil 386 } 387 388 func (c *RemoteClient) writeLockInfo(info *state.LockInfo, deleteAfter time.Duration, ifNoneMatch string) error { 389 err := c.put(c.lockFilePath(), info.Marshal(), int(deleteAfter.Seconds()), ifNoneMatch) 390 391 if httpErr, ok := err.(gophercloud.ErrUnexpectedResponseCode); ok && httpErr.Actual == 412 { 392 log.Printf("[DEBUG] Couldn't write lock %s. One already exists.", info.ID) 393 info2, err2 := c.lockInfo() 394 if err2 != nil { 395 return fmt.Errorf("Couldn't read lock info: %v", err2) 396 } 397 398 return c.lockError(err, info2) 399 } 400 401 if err != nil { 402 return c.lockError(err, nil) 403 } 404 405 return nil 406 } 407 408 func (c *RemoteClient) lockError(err error, conflictingLock *state.LockInfo) *state.LockError { 409 lockErr := &state.LockError{ 410 Err: err, 411 Info: conflictingLock, 412 } 413 414 return lockErr 415 } 416 417 // lockInfo reads the lock file, parses its contents and returns the parsed 418 // LockInfo struct. 419 func (c *RemoteClient) lockInfo() (*state.LockInfo, error) { 420 raw, err := c.get(c.lockFilePath()) 421 if err != nil { 422 return nil, err 423 } 424 425 info := &state.LockInfo{} 426 427 if err := json.Unmarshal(raw.Data, info); err != nil { 428 return nil, err 429 } 430 431 return info, nil 432 } 433 434 func (c *RemoteClient) lockRenewPeriodic(ctx context.Context, info *state.LockInfo) error { 435 log.Printf("[DEBUG] Renew lock %v", info) 436 437 waitDur := lockRenewInterval 438 lastRenewTime := time.Now() 439 var lastErr error 440 for { 441 if time.Since(lastRenewTime) > lockTTL { 442 return lastErr 443 } 444 select { 445 case <-time.After(waitDur): 446 c.mu.Lock() 447 // Unlock may have released the mu.Lock 448 // in which case we shouldn't renew the lock 449 select { 450 case <-ctx.Done(): 451 log.Printf("[DEBUG] Stopping Periodic renew of lock %v", info) 452 return nil 453 default: 454 } 455 456 info2, err := c.lockInfo() 457 if _, ok := err.(gophercloud.ErrDefault404); ok { 458 log.Println("[DEBUG] Lock has expired trying to reacquire.") 459 err = nil 460 } 461 462 if err == nil && (info2 == nil || info.ID == info2.ID) { 463 info2 = info 464 log.Printf("[DEBUG] Renewing lock %v.", info) 465 err = c.writeLockInfo(info, lockTTL, "") 466 } 467 468 c.mu.Unlock() 469 470 if err != nil { 471 log.Printf("[ERROR] could not reacquire lock (%v): %s", info, err) 472 waitDur = time.Second 473 lastErr = err 474 continue 475 } 476 477 // conflicting lock 478 if info2.ID != info.ID { 479 return c.lockError(fmt.Errorf("lock id %q does not match existing lock %q", info.ID, info2.ID), info2) 480 } 481 482 waitDur = lockRenewInterval 483 lastRenewTime = time.Now() 484 485 case <-ctx.Done(): 486 log.Printf("[DEBUG] Stopping Periodic renew of lock %s", info.ID) 487 return nil 488 } 489 } 490 } 491 492 func (c *RemoteClient) lockFilePath() string { 493 return c.objectName + lockSuffix 494 } 495 496 func (c *RemoteClient) ensureContainerExists() error { 497 containerOpts := &containers.CreateOpts{} 498 499 if c.archive { 500 log.Printf("[DEBUG] Creating archive container %s", c.archiveContainer) 501 result := containers.Create(c.client, c.archiveContainer, nil) 502 if result.Err != nil { 503 log.Printf("[DEBUG] Error creating archive container %s: %s", c.archiveContainer, result.Err) 504 return result.Err 505 } 506 507 log.Printf("[DEBUG] Enabling Versioning on container %s", c.container) 508 containerOpts.VersionsLocation = c.archiveContainer 509 } 510 511 log.Printf("[DEBUG] Creating container %s", c.container) 512 result := containers.Create(c.client, c.container, containerOpts) 513 if result.Err != nil { 514 return result.Err 515 } 516 517 return nil 518 } 519 520 // Helper function to get a list of objects in a Swift container 521 func (c *RemoteClient) objectNames(container string) (objectNames []string, err error) { 522 _ = objects.List(c.client, container, nil).EachPage(func(page pagination.Page) (bool, error) { 523 // Get a slice of object names 524 names, err := objects.ExtractNames(page) 525 if err != nil { 526 return false, fmt.Errorf("Error extracting object names from page: %s", err) 527 } 528 for _, object := range names { 529 objectNames = append(objectNames, object) 530 } 531 532 return true, nil 533 }) 534 return 535 }