1package swift
2
3import (
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/internal/states/remote"
18	"github.com/hashicorp/terraform/internal/states/statemgr"
19)
20
21const (
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
44type 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 *statemgr.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
64func (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
98func (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
113func (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
123func (c *RemoteClient) Delete() error {
124	return c.delete(c.objectName)
125}
126
127func (c *RemoteClient) Lock(info *statemgr.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
165func (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 := `
213WARNING: Waiting for lock deletion timed out.
214Swift has accepted the deletion order of the lock %s/%s.
215But as it is eventually consistent, complete deletion
216may 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
251func (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
275func (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
306func (c *RemoteClient) deleteContainer() error {
307	log.Printf("[DEBUG] Deleting container %s", c.container)
308
309	warning := `
310WARNING: Waiting for container %s deletion timed out.
311It may have been left in your Openstack account and may incur storage charges.
312error 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
354func (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
377func (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
388func (c *RemoteClient) writeLockInfo(info *statemgr.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
408func (c *RemoteClient) lockError(err error, conflictingLock *statemgr.LockInfo) *statemgr.LockError {
409	lockErr := &statemgr.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.
419func (c *RemoteClient) lockInfo() (*statemgr.LockInfo, error) {
420	raw, err := c.get(c.lockFilePath())
421	if err != nil {
422		return nil, err
423	}
424
425	info := &statemgr.LockInfo{}
426
427	if err := json.Unmarshal(raw.Data, info); err != nil {
428		return nil, err
429	}
430
431	return info, nil
432}
433
434func (c *RemoteClient) lockRenewPeriodic(ctx context.Context, info *statemgr.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
492func (c *RemoteClient) lockFilePath() string {
493	return c.objectName + lockSuffix
494}
495
496func (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
521func (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}
536