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  }