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 }