github.com/hernad/nomad@v1.6.112/helper/snapshot/snapshot.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  // snapshot manages the interactions between Nomad and Raft in order to take
     5  // and restore snapshots for disaster recovery. The internal format of a
     6  // snapshot is simply a tar file, as described in archive.go.
     7  package snapshot
     8  
     9  import (
    10  	"compress/gzip"
    11  	"crypto/sha256"
    12  	"encoding/base64"
    13  	"fmt"
    14  	"io"
    15  	"os"
    16  
    17  	"github.com/hashicorp/go-hclog"
    18  	"github.com/hashicorp/raft"
    19  )
    20  
    21  // Snapshot is a structure that holds state about a temporary file that is used
    22  // to hold a snapshot. By using an intermediate file we avoid holding everything
    23  // in memory.
    24  type Snapshot struct {
    25  	file     *os.File
    26  	index    uint64
    27  	checksum string
    28  }
    29  
    30  // New takes a state snapshot of the given Raft instance into a temporary file
    31  // and returns an object that gives access to the file as an io.Reader. You must
    32  // arrange to call Close() on the returned object or else you will leak a
    33  // temporary file.
    34  func New(logger hclog.Logger, r *raft.Raft) (*Snapshot, error) {
    35  	// Take the snapshot.
    36  	future := r.Snapshot()
    37  	if err := future.Error(); err != nil {
    38  		return nil, fmt.Errorf("Raft error when taking snapshot: %v", err)
    39  	}
    40  
    41  	// Open up the snapshot.
    42  	metadata, snap, err := future.Open()
    43  	if err != nil {
    44  		return nil, fmt.Errorf("failed to open snapshot: %v:", err)
    45  	}
    46  	defer func() {
    47  		if err := snap.Close(); err != nil {
    48  			logger.Error("Failed to close Raft snapshot", "error", err)
    49  		}
    50  	}()
    51  
    52  	// Make a scratch file to receive the contents so that we don't buffer
    53  	// everything in memory. This gets deleted in Close() since we keep it
    54  	// around for re-reading.
    55  	archive, err := os.CreateTemp("", "snapshot")
    56  	if err != nil {
    57  		return nil, fmt.Errorf("failed to create snapshot file: %v", err)
    58  	}
    59  
    60  	// If anything goes wrong after this point, we will attempt to clean up
    61  	// the temp file. The happy path will disarm this.
    62  	var keep bool
    63  	defer func() {
    64  		if keep {
    65  			return
    66  		}
    67  
    68  		if err := os.Remove(archive.Name()); err != nil {
    69  			logger.Error("Failed to clean up temp snapshot", "error", err)
    70  		}
    71  	}()
    72  
    73  	hash := sha256.New()
    74  	out := io.MultiWriter(hash, archive)
    75  
    76  	// Wrap the file writer in a gzip compressor.
    77  	compressor := gzip.NewWriter(out)
    78  
    79  	// Write the archive.
    80  	if err := write(compressor, metadata, snap); err != nil {
    81  		return nil, fmt.Errorf("failed to write snapshot file: %v", err)
    82  	}
    83  
    84  	// Finish the compressed stream.
    85  	if err := compressor.Close(); err != nil {
    86  		return nil, fmt.Errorf("failed to compress snapshot file: %v", err)
    87  	}
    88  
    89  	// Sync the compressed file and rewind it so it's ready to be streamed
    90  	// out by the caller.
    91  	if err := archive.Sync(); err != nil {
    92  		return nil, fmt.Errorf("failed to sync snapshot: %v", err)
    93  	}
    94  	if _, err := archive.Seek(0, 0); err != nil {
    95  		return nil, fmt.Errorf("failed to rewind snapshot: %v", err)
    96  	}
    97  
    98  	checksum := "sha-256=" + base64.StdEncoding.EncodeToString(hash.Sum(nil))
    99  
   100  	keep = true
   101  	return &Snapshot{archive, metadata.Index, checksum}, nil
   102  }
   103  
   104  // Index returns the index of the snapshot. This is safe to call on a nil
   105  // snapshot, it will just return 0.
   106  func (s *Snapshot) Index() uint64 {
   107  	if s == nil {
   108  		return 0
   109  	}
   110  	return s.index
   111  }
   112  
   113  func (s *Snapshot) Checksum() string {
   114  	if s == nil {
   115  		return ""
   116  	}
   117  	return s.checksum
   118  }
   119  
   120  // Read passes through to the underlying snapshot file. This is safe to call on
   121  // a nil snapshot, it will just return an EOF.
   122  func (s *Snapshot) Read(p []byte) (n int, err error) {
   123  	if s == nil {
   124  		return 0, io.EOF
   125  	}
   126  	return s.file.Read(p)
   127  }
   128  
   129  // Close closes the snapshot and removes any temporary storage associated with
   130  // it. You must arrange to call this whenever NewSnapshot() has been called
   131  // successfully. This is safe to call on a nil snapshot.
   132  func (s *Snapshot) Close() error {
   133  	if s == nil {
   134  		return nil
   135  	}
   136  
   137  	if err := s.file.Close(); err != nil {
   138  		return err
   139  	}
   140  	return os.Remove(s.file.Name())
   141  }
   142  
   143  type Discard struct {
   144  	io.Writer
   145  }
   146  
   147  func (dc Discard) Close() error { return nil }
   148  
   149  // Verify takes the snapshot from the reader and verifies its contents.
   150  func Verify(in io.Reader) (*raft.SnapshotMeta, error) {
   151  	return CopySnapshot(in, Discard{Writer: io.Discard})
   152  }
   153  
   154  // CopySnapshot copies the snapshot content from snapshot archive to dest.
   155  // It will close the destination once complete.
   156  func CopySnapshot(in io.Reader, dest io.WriteCloser) (*raft.SnapshotMeta, error) {
   157  	defer dest.Close()
   158  
   159  	// Wrap the reader in a gzip decompressor.
   160  	decomp, err := gzip.NewReader(in)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("failed to decompress snapshot: %v", err)
   163  	}
   164  	defer decomp.Close()
   165  
   166  	// Read the archive, throwing away the snapshot data.
   167  	var metadata raft.SnapshotMeta
   168  	if err := read(decomp, &metadata, dest); err != nil {
   169  		return nil, fmt.Errorf("failed to read snapshot file: %v", err)
   170  	}
   171  
   172  	if err := concludeGzipRead(decomp); err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	return &metadata, nil
   177  }
   178  
   179  // concludeGzipRead should be invoked after you think you've consumed all of
   180  // the data from the gzip stream. It will error if the stream was corrupt.
   181  //
   182  // The docs for gzip.Reader say: "Clients should treat data returned by Read as
   183  // tentative until they receive the io.EOF marking the end of the data."
   184  func concludeGzipRead(decomp *gzip.Reader) error {
   185  	extra, err := io.ReadAll(decomp) // ReadAll consumes the EOF
   186  	if err != nil {
   187  		return err
   188  	} else if len(extra) != 0 {
   189  		return fmt.Errorf("%d unread uncompressed bytes remain", len(extra))
   190  	}
   191  	return nil
   192  }
   193  
   194  type readWrapper struct {
   195  	in io.Reader
   196  	c  int
   197  }
   198  
   199  func (r *readWrapper) Read(b []byte) (int, error) {
   200  	n, err := r.in.Read(b)
   201  	r.c += n
   202  	if err != nil && err != io.EOF {
   203  		return n, fmt.Errorf("failed to read after %v: %v", r.c, err)
   204  	}
   205  	return n, err
   206  }
   207  
   208  // Restore takes the snapshot from the reader and attempts to apply it to the
   209  // given Raft instance.
   210  func Restore(logger hclog.Logger, in io.Reader, r *raft.Raft) error {
   211  	// Wrap the reader in a gzip decompressor.
   212  	decomp, err := gzip.NewReader(&readWrapper{in, 0})
   213  	if err != nil {
   214  		return fmt.Errorf("failed to decompress snapshot: %v", err)
   215  	}
   216  	defer func() {
   217  		if err := decomp.Close(); err != nil {
   218  			logger.Error("Failed to close snapshot decompressor", "error", err)
   219  		}
   220  	}()
   221  
   222  	// Make a scratch file to receive the contents of the snapshot data so
   223  	// we can avoid buffering in memory.
   224  	snap, err := os.CreateTemp("", "snapshot")
   225  	if err != nil {
   226  		return fmt.Errorf("failed to create temp snapshot file: %v", err)
   227  	}
   228  	defer func() {
   229  		if err := snap.Close(); err != nil {
   230  			logger.Error("Failed to close temp snapshot", "error", err)
   231  		}
   232  		if err := os.Remove(snap.Name()); err != nil {
   233  			logger.Error("Failed to clean up temp snapshot", "error", err)
   234  		}
   235  	}()
   236  
   237  	// Read the archive.
   238  	var metadata raft.SnapshotMeta
   239  	if err := read(decomp, &metadata, snap); err != nil {
   240  		return fmt.Errorf("failed to read snapshot file: %v", err)
   241  	}
   242  
   243  	if err := concludeGzipRead(decomp); err != nil {
   244  		return err
   245  	}
   246  
   247  	// Sync and rewind the file so it's ready to be read again.
   248  	if err := snap.Sync(); err != nil {
   249  		return fmt.Errorf("failed to sync temp snapshot: %v", err)
   250  	}
   251  	if _, err := snap.Seek(0, 0); err != nil {
   252  		return fmt.Errorf("failed to rewind temp snapshot: %v", err)
   253  	}
   254  
   255  	// Feed the snapshot into Raft.
   256  	if err := r.Restore(&metadata, snap, 0); err != nil {
   257  		return fmt.Errorf("Raft error when restoring snapshot: %v", err)
   258  	}
   259  
   260  	return nil
   261  }